diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3434263 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) Miguel Piedrafita + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..03ea46d --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# chatgpt-go + +ChatGPT SDK 是用 Golang 编写的,可以用于快速集成 ChatGPT 的聊天机器人功能。 + +## 快速开始 + +1. 安装 ChatGPT-Go SDK。 + +```shell +go get -u github.com/xyhelper/chatgpt-go +``` + +2. 在独立的对话中进行聊天。 + +```go +package main + +import ( + "log" + "time" + + chatgpt "github.com/xyhelper/chatgpt-go" +) + +func main() { + token := `random token` + + cli := chatgpt.NewClient( + chatgpt.WithDebug(true), // 开启调试模式 + chatgpt.WithTimeout(120*time.Second), // 设置超时时间为120秒 + chatgpt.WithAccessToken(token), // 设置token + chatgpt.WithBaseURI("https://freechat.lidong.xin"), // 设置base uri + ) + + // chat in independent conversation + message := "你好" + text, err := cli.GetChatText(message) + if err != nil { + log.Fatalf("get chat text failed: %v", err) + } + + log.Printf("q: %s, a: %s\n", message, text.Content) +} + +``` + +3. 在连续的对话中进行聊天。 + +```go +package main + +import ( + "log" + "time" + + chatgpt "github.com/xyhelper/chatgpt-go" +) + +func main() { + // new chatgpt client + token := `random token` + + cli := chatgpt.NewClient( + chatgpt.WithDebug(true), + chatgpt.WithTimeout(60*time.Second), + chatgpt.WithAccessToken(token), + chatgpt.WithBaseURI("https://freechat.lidong.xin"), + ) + + // chat in continuous conversation + + // first message + message := "对我说你好" + text, err := cli.GetChatText(message) + if err != nil { + log.Fatalf("get chat text failed: %v", err) + } + + log.Printf("q: %s, a: %s\n", message, text.Content) + + // continue conversation with new message + conversationID := text.ConversationID + parentMessage := text.MessageID + newMessage := "再说一次" + + newText, err := cli.GetChatText(newMessage, conversationID, parentMessage) + if err != nil { + log.Fatalf("get chat text failed: %v", err) + } + + log.Printf("q: %s, a: %s\n", newMessage, newText.Content) +} +``` + +> 如果你想要在当前对话之外开始一个新的对话,你不需要重置客户端。只需在`GetChatText`方法中移除`conversationID`和`parentMessage`参数,即可获得一个新的文本回复,从而开始一个新的对话。 + +4. 使用流(stream)获取聊天内容。 + +```go +package main + +import ( + "log" + "time" + + chatgpt "github.com/xyhelper/chatgpt-go" +) + +func main() { + // new chatgpt client + token := `random token` + + cli := chatgpt.NewClient( + chatgpt.WithDebug(true), + chatgpt.WithTimeout(120*time.Second), + chatgpt.WithAccessToken(token), + chatgpt.WithBaseURI("https://freechat.xyhelper.cn"), + ) + + message := "你好" + stream, err := cli.GetChatStream(message) + if err != nil { + log.Fatalf("get chat stream failed: %v\n", err) + } + + var answer string + for text := range stream.Stream { + log.Printf("stream text: %s\n", text.Content) + + answer = text.Content + } + + if stream.Err != nil { + log.Fatalf("stream closed with error: %v\n", stream.Err) + } + + log.Printf("q: %s, a: %s\n", message, answer) +} + +``` + +## DEMO + +cli/chatgpt-go 是一个使用 ChatGPT-Go SDK 的示例程序。 +可以使用以下命令运行它: + +```shell +cd cli/chatgpt-go +go run main.go +``` + +也可以使用 go install 命令安装它: + +```shell +go install github.com/xyhelper/chatgpt-go/cli/chatgpt-go@latest +``` + +然后运行: + +```shell +chatgpt-go +``` + +## 作品演示 + +[https://xyhelper.cn](https;//xyhelper.cn) + +## 友情链接 + +- [CoolAdmin](https://cool-js.com) - 一个项目,用 COOL 就够了。AI 编码、物联网、函数云开发等更加智能和全面的功能,让应用开发紧跟时代的步伐,cool 绝不落后!!! diff --git a/chat.go b/chat.go new file mode 100644 index 0000000..cee5057 --- /dev/null +++ b/chat.go @@ -0,0 +1,195 @@ +package chatgpt + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/google/uuid" + "github.com/launchdarkly/eventsource" + "github.com/tidwall/gjson" +) + +// ChatText chat reply with text format +type ChatText struct { + data string // event data + ConversationID string // conversation context id + MessageID string // current message id, can used as next chat's parent_message_id + Content string // text content +} + +// ChatStream chat reply with sream +type ChatStream struct { + Stream chan *ChatText // chat text stream + Err error // error message +} + +// ChatText format to string +func (c *ChatText) String() string { + return c.data +} + +// GetChatText will return text message +func (c *Client) GetChatText(message string, args ...string) (*ChatText, error) { + resp, err := c.sendMessage(message, args...) + if err != nil { + return nil, fmt.Errorf("send message failed: %v", err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read response body failed: %v", err) + } + + arr := strings.Split(string(body), "\n\n") + + const TEXT_ARR_MIN_LEN = 3 + const TEXT_STR_MIN_LEN = 6 + + l := len(arr) + if l < TEXT_ARR_MIN_LEN { + return nil, fmt.Errorf("invalid reply message: %s", body) + } + + str := arr[l-TEXT_ARR_MIN_LEN] + if len(str) < TEXT_STR_MIN_LEN { + return nil, fmt.Errorf("invalid reply message: %s", body) + } + + text := str[TEXT_STR_MIN_LEN:] + + return c.parseChatText(text) +} + +// GetChatStream will return text stream +func (c *Client) GetChatStream(message string, args ...string) (*ChatStream, error) { + resp, err := c.sendMessage(message, args...) + if err != nil { + return nil, fmt.Errorf("send message failed: %v", err) + } + + chatStream := &ChatStream{ + Stream: make(chan *ChatText), + Err: nil, + } + + decoder := eventsource.NewDecoder(resp.Body) + + go func() { + defer resp.Body.Close() + defer close(chatStream.Stream) + + for { + event, err := decoder.Decode() + if err != nil { + chatStream.Err = fmt.Errorf("decode data failed: %v", err) + return + } + + text := event.Data() + if text == "" || text == EOF_TEXT { + // read data finished, success return + return + } + + chatText, err := c.parseChatText(text) + if err != nil { + continue + } + + chatStream.Stream <- chatText + } + }() + + return chatStream, nil +} + +// parseChatText will return a ChatText struct from ChatText json +func (c *Client) parseChatText(text string) (*ChatText, error) { + if text == "" || text == EOF_TEXT { + return nil, fmt.Errorf("invalid chat text: %s", text) + } + + res := gjson.Parse(text) + + conversationID := res.Get("conversation_id").String() + messageID := res.Get("message.id").String() + content := res.Get("message.content.parts.0").String() + + if conversationID == "" || messageID == "" { + return nil, fmt.Errorf("invalid chat text") + } + + return &ChatText{ + data: text, + ConversationID: conversationID, + MessageID: messageID, + Content: content, + }, nil +} + +// sendMessage will send message to ChatGPT server +func (c *Client) sendMessage(message string, args ...string) (*http.Response, error) { + accessToken, err := c.getAccessToken() + if err != nil { + return nil, fmt.Errorf("get accessToken failed: %v", err) + } + + var messageID string + var conversationID string + var parentMessageID string + + messageID = uuid.NewString() + if len(args) > 0 { + conversationID = args[0] + } + if len(args) > 1 { + parentMessageID = args[1] + } + if parentMessageID == "" { + parentMessageID = uuid.NewString() + } + + params := MixMap{ + "action": "next", + "model": c.opts.Model, + "parent_message_id": parentMessageID, + "messages": []MixMap{ + { + "role": "user", + "id": messageID, + "content": MixMap{ + "content_type": "text", + "parts": []string{message}, + }, + }, + }, + } + + if conversationID != "" { + params["conversation_id"] = conversationID + } + + data, err := json.Marshal(params) + if err != nil { + return nil, fmt.Errorf("marshal request body failed: %v", err) + } + + req, err := http.NewRequest(http.MethodPost, c.baseURI+"/backend-api/conversation", bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("new request failed: %v", err) + } + + bearerToken := fmt.Sprintf("Bearer %s", accessToken) + req.Header.Set("Authorization", bearerToken) + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("Content-Type", "application/json") + + resp, err := c.doRequest(req) + + return resp, err +} diff --git a/cli/chatgpt-go/main.go b/cli/chatgpt-go/main.go new file mode 100644 index 0000000..3a9a3a2 --- /dev/null +++ b/cli/chatgpt-go/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "strings" + "time" + + "github.com/google/uuid" + "github.com/xyhelper/chatgpt-go" +) + +func main() { + token := uuid.New().String() + cli := chatgpt.NewClient( + // chatgpt.WithDebug(true), + chatgpt.WithTimeout(120*time.Second), + chatgpt.WithAccessToken(token), + chatgpt.WithBaseURI("https://freechat.lidong.xin"), + ) + conversationID := "" + parentMessage := "" + + reader := bufio.NewReader(os.Stdin) + for { + fmt.Print("请输入您的消息:") + message, err := reader.ReadString('\n') + if err != nil { + fmt.Println("读取输入失败:", err) + continue + } + stream, err := cli.GetChatStream(message, conversationID, parentMessage) + if err != nil { + log.Fatalf("get chat stream failed: %v\n", err) + } + + var answer string + for text := range stream.Stream { + // log.Printf("stream text: %s\n", text.Content) + print(strings.Replace(text.Content, answer, "", 1)) + + answer = text.Content + conversationID = text.ConversationID + parentMessage = text.MessageID + } + + if stream.Err != nil { + log.Fatalf("stream closed with error: %v\n", stream.Err) + } + // 输出换行 + println() + } +} + +// GetStream will return text stream diff --git a/client.go b/client.go new file mode 100644 index 0000000..adf62bf --- /dev/null +++ b/client.go @@ -0,0 +1,119 @@ +package chatgpt + +import ( + "errors" + "log" + "net/http" + "net/http/httputil" + "net/url" + "time" +) + +const ( + BASE_URI = "https://freechat.xyhelper.cn" + // AUTH_SESSION_URI = "https://chat.openai.com/api/auth/session" + // CONVERSATION_URI = "https://chat.openai.lidong.xin/backend-api/conversation" + USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" + EOF_TEXT = "[DONE]" +) + +// MixMap is a type alias for map[string]interface{} +type MixMap = map[string]interface{} + +// Client is a ChatGPT request client +type Client struct { + opts Options // custom options + httpCli *http.Client + baseURI string +} + +// NewClient will return a ChatGPT request client +func NewClient(options ...Option) *Client { + cli := &Client{ + opts: Options{ + Timeout: 30 * time.Second, // set default timeout + UserAgent: USER_AGENT, // set default user-agent + Model: "text-davinci-002-render-sha", // set default chat model + }, + } + + // load custom options + for _, option := range options { + option(cli) + } + if cli.baseURI == "" { + cli.baseURI = BASE_URI + } + + cli.initHttpClient() + + return cli +} + +func (c *Client) initHttpClient() { + transport := &http.Transport{} + + if c.opts.Proxy != "" { + proxy, err := url.Parse(c.opts.Proxy) + if err == nil { + transport.Proxy = http.ProxyURL(proxy) + } + } + + c.httpCli = &http.Client{ + Timeout: c.opts.Timeout, + Transport: transport, + } +} + +// SetModel will set the chat model +func (c *Client) SetModel(model string) { + c.opts.Model = model +} + +// SetProxy will set the proxy +func (c *Client) SetProxy(proxy string) { + transport := &http.Transport{} + if proxy != "" { + proxyURL, err := url.Parse(proxy) + if err == nil { + transport.Proxy = http.ProxyURL(proxyURL) + } + } + c.httpCli.Transport = transport +} + +func (c *Client) doRequest(req *http.Request) (*http.Response, error) { + if c.opts.UserAgent != "" { + req.Header.Set("User-Agent", c.opts.UserAgent) + } + if c.opts.Cookie != "" { + req.Header.Set("Cookie", c.opts.Cookie) + } else if len(c.opts.Cookies) > 0 { + for _, cookie := range c.opts.Cookies { + req.AddCookie(cookie) + } + } + + if c.opts.Debug { + reqInfo, _ := httputil.DumpRequest(req, true) + log.Printf("http request info: \n%s\n", reqInfo) + } + + resp, err := c.httpCli.Do(req) + if err != nil { + log.Printf("http request failed: %v\n", err) + return nil, err + } + if resp.StatusCode != http.StatusOK { + log.Printf("http response status: %s\n", resp.Status) + return nil, errors.New(resp.Status) + } + + if c.opts.Debug { + respInfo, _ := httputil.DumpResponse(resp, true) + log.Printf("http response info: \n%s\n", respInfo) + } + + return resp, err +} diff --git a/examples/chat_test.go b/examples/chat_test.go new file mode 100644 index 0000000..8426bc8 --- /dev/null +++ b/examples/chat_test.go @@ -0,0 +1,80 @@ +package examples + +import ( + "fmt" + "log" +) + +func ExampleClient_GetChatText() { + message := "say hello to me" + + log.Printf("start get chat text") + + // chat in independent conversation + text, err := cli.GetChatText(message) + if err != nil { + log.Fatalf("get chat text failed: %v", err) + } + + log.Printf("\nq: %s\na: %s\n", message, text.Content) + + fmt.Println("xxx") + // Output: xxx +} + +func ExampleClient_GetContinuousChatText() { + message := "say hello to me" + + log.Printf("start get chat text") + + // chat in independent conversation + text, err := cli.GetChatText(message) + if err != nil { + log.Fatalf("get chat text failed: %v", err) + } + + log.Printf("\nq: %s\na: %s\n", message, text.Content) + + log.Printf("start get chat text again") + + // continue conversation with new message + conversationID := text.ConversationID + parentMessage := text.MessageID + newMessage := "again" + + newText, err := cli.GetChatText(newMessage, conversationID, parentMessage) + if err != nil { + log.Fatalf("get chat text failed: %v", err) + } + + log.Printf("\nq: %s\na: %s\n", newMessage, newText.Content) + + fmt.Println("xxx") + // Output: xxx +} + +func ExampleClient_GetChatStream() { + message := "say hello to me" + + log.Printf("start get chat stream") + + stream, err := cli.GetChatStream(message) + if err != nil { + log.Fatalf("get chat stream failed: %v\n", err) + } + + var answer string + for text := range stream.Stream { + log.Printf("stream text: %s\n", text.Content) + answer = text.Content + } + + if stream.Err != nil { + log.Fatalf("stream closed with error: %v\n", stream.Err) + } + + log.Printf("\nq: %s\na: %s\n", message, answer) + + fmt.Println("xxx") + // Output: xxx +} diff --git a/examples/client_test.go b/examples/client_test.go new file mode 100644 index 0000000..f9c33fd --- /dev/null +++ b/examples/client_test.go @@ -0,0 +1,27 @@ +package examples + +import ( + "fmt" + "time" + + chatgpt "github.com/xyhelper/chatgpt-go" +) + +// chatgpt client +var cli *chatgpt.Client + +func ExampleNewClient() { + fmt.Printf("%T", cli) + + // Output: *chatgpt.Client +} + +func init() { + token := `copy-from-cookies` + + cli = chatgpt.NewClient( + chatgpt.WithDebug(false), + chatgpt.WithTimeout(60*time.Second), + chatgpt.WithAccessToken(token), + ) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0798401 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/xyhelper/chatgpt-go + +go 1.18 + +require ( + github.com/gogf/gf/v2 v2.3.3 + github.com/google/uuid v1.3.0 + github.com/launchdarkly/eventsource v1.7.1 + github.com/tidwall/gjson v1.14.4 +) + +require ( + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2177b74 --- /dev/null +++ b/go.sum @@ -0,0 +1,82 @@ +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogf/gf/v2 v2.3.3 h1:3iry6kybjvuryTtjypG9oUuxrQ0URMT7j0DVg7FFnaw= +github.com/gogf/gf/v2 v2.3.3/go.mod h1:tsbmtwcAl2chcYoq/fP9W2FZf06aw4i89X34nbSHo9Y= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= +github.com/launchdarkly/eventsource v1.7.1 h1:StoRQeiPyrcQIXjlQ7b5jWMzHW4p+GGczN2r2oBhujg= +github.com/launchdarkly/eventsource v1.7.1/go.mod h1:LHxSeb4OnqznNZxCSXbFghxS/CjIQfzHovNoAqbO/Wk= +github.com/launchdarkly/go-test-helpers/v2 v2.2.0 h1:L3kGILP/6ewikhzhdNkHy1b5y4zs50LueWenVF0sBbs= +github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opentelemetry.io/otel v1.7.0 h1:Z2lA3Tdch0iDcrhJXDIlC94XE+bxok1F9B+4Lz/lGsM= +go.opentelemetry.io/otel v1.7.0/go.mod h1:5BdUoMIz5WEs0vt0CUEMtSSaTSHBBVwrhnz7+nrD5xk= +go.opentelemetry.io/otel/sdk v1.7.0/go.mod h1:uTEOTwaqIVuTGiJN7ii13Ibp75wJmYUDe374q6cZwUU= +go.opentelemetry.io/otel/trace v1.7.0 h1:O37Iogk1lEkMRXewVtZ1BBTVn5JEp8GrJvP92bJqC6o= +go.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/images/chat_stream.png b/images/chat_stream.png new file mode 100644 index 0000000..aeb9aa7 Binary files /dev/null and b/images/chat_stream.png differ diff --git a/images/chat_text.png b/images/chat_text.png new file mode 100644 index 0000000..d8a5656 Binary files /dev/null and b/images/chat_text.png differ diff --git a/images/chatgpt_cookies.png b/images/chatgpt_cookies.png new file mode 100644 index 0000000..bb6b7a2 Binary files /dev/null and b/images/chatgpt_cookies.png differ diff --git a/images/continuous_chat_text.png b/images/continuous_chat_text.png new file mode 100644 index 0000000..62e85b4 Binary files /dev/null and b/images/continuous_chat_text.png differ diff --git a/images/wechat_group.jpg b/images/wechat_group.jpg new file mode 100644 index 0000000..e9ecc94 Binary files /dev/null and b/images/wechat_group.jpg differ diff --git a/option.go b/option.go new file mode 100644 index 0000000..46738d0 --- /dev/null +++ b/option.go @@ -0,0 +1,92 @@ +package chatgpt + +import ( + "net/http" + "time" +) + +// Options can set custom options for ChatGPT request client +type Options struct { + // Debug is used to output debug message + Debug bool + // Timeout is used to end http request after timeout duration + Timeout time.Duration + // UserAgent is used for custom user-agent + UserAgent string + // Cookies is request cookies for each api + Cookies []*http.Cookie + // Cookie will set in request headers with string format + Cookie string + // Proxy is used to proxy request + Proxy string + // AccessToken is used to authorization + AccessToken string + // Model is the chat model + Model string +} + +// Option is used to set custom option +type Option func(*Client) + +// WithDebug is used to output debug message +func WithDebug(debug bool) Option { + return func(c *Client) { + c.opts.Debug = debug + } +} + +// WithTimeout is used to set request timeout +func WithTimeout(timeout time.Duration) Option { + return func(c *Client) { + c.opts.Timeout = timeout + } +} + +// WithUserAgent is used to set request user-agent +func WithUserAgent(userAgent string) Option { + return func(c *Client) { + c.opts.UserAgent = userAgent + } +} + +// WithCookies is used to set request cookies +func WithCookies(cookies []*http.Cookie) Option { + return func(c *Client) { + c.opts.Cookies = cookies + } +} + +// WithCookie is used to set request cookies in header +func WithCookie(cookie string) Option { + return func(c *Client) { + c.opts.Cookie = cookie + } +} + +// WithProxy is used to set request proxy +func WithProxy(proxy string) Option { + return func(c *Client) { + c.opts.Proxy = proxy + } +} + +// WithAccessToken is used to set accessToken +func WithAccessToken(accessToken string) Option { + return func(c *Client) { + c.opts.AccessToken = accessToken + } +} + +// WithModel is used to set chat model +func WithModel(model string) Option { + return func(c *Client) { + c.opts.Model = model + } +} + +// WithBaseURI is used to set base uri +func WithBaseURI(baseURI string) Option { + return func(c *Client) { + c.baseURI = baseURI + } +} diff --git a/session.go b/session.go new file mode 100644 index 0000000..48a1273 --- /dev/null +++ b/session.go @@ -0,0 +1,28 @@ +package chatgpt + +// authSession will check if session is expired and return a new accessToken +// func (c *Client) authSession() (*gjson.Result, error) { +// req, err := http.NewRequest(http.MethodGet, AUTH_SESSION_URI, nil) +// if err != nil { +// return nil, fmt.Errorf("new request failed: %v", err) +// } + +// resp, err := c.doRequest(req) + +// if err != nil { +// return nil, fmt.Errorf("do request failed: %v", err) +// } +// defer resp.Body.Close() + +// body, err := ioutil.ReadAll(resp.Body) +// if err != nil { +// return nil, fmt.Errorf("read response body failed: %v", err) +// } + +// res := gjson.ParseBytes(body) +// if res.String() == "" { +// return nil, fmt.Errorf("parse response body failed") +// } + +// return &res, nil +// } diff --git a/token.go b/token.go new file mode 100644 index 0000000..eb96012 --- /dev/null +++ b/token.go @@ -0,0 +1,24 @@ +package chatgpt + +import "github.com/gogf/gf/v2/errors/gerror" + +// getAccessToken will return accessToken, if expired than fetch a new one +func (c *Client) getAccessToken() (string, error) { + if c.opts.AccessToken != "" { + return c.opts.AccessToken, nil + } + return "", gerror.New("accessToken is empty") + + // // fetch new accessToken + // res, err := c.authSession() + // if err != nil { + // return "", fmt.Errorf("fetch new accessToken failed: %v", err) + // } + + // accessToken := res.Get("accessToken").String() + // if accessToken == "" { + // return "", fmt.Errorf("invalid session data: %s", accessToken) + // } + + // return accessToken, nil +}