├── .env.example ├── go.mod ├── larkgpt ├── metrics.go ├── lark.go ├── chatgpt_test.go ├── client.go ├── larkbot.go └── chatgpt.go ├── .gitignore ├── main.go └── go.sum /.env.example: -------------------------------------------------------------------------------- 1 | APP_ID= 2 | APP_SECRET= 3 | CHATGPT_API_KEY= 4 | CHATGPT_API_URL= 5 | ENABLE_SESSION_FOR_LARK_GROUP=true -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bytemate/larkgpt 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/avast/retry-go v3.0.0+incompatible 7 | github.com/chyroc/lark v0.0.104 8 | github.com/go-resty/resty/v2 v2.7.0 9 | github.com/joho/godotenv v1.4.0 10 | ) 11 | 12 | require golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect 13 | -------------------------------------------------------------------------------- /larkgpt/metrics.go: -------------------------------------------------------------------------------- 1 | package larkgpt 2 | 3 | type IMetrics interface { 4 | // larkgpt api 5 | EmitChatGPTApiFailed() 6 | EmitChatGPTApiSuccess() 7 | 8 | // lark api 9 | EmitLarkApiFailed() 10 | EmitLarkApiSuccess() 11 | 12 | // app 13 | EmitAppSuccess() 14 | EmitAppFailed() 15 | } 16 | 17 | type noneMetrics struct{} 18 | 19 | func (r *noneMetrics) EmitChatGPTApiFailed() {} 20 | 21 | func (r *noneMetrics) EmitChatGPTApiSuccess() {} 22 | 23 | func (r *noneMetrics) EmitLarkApiFailed() {} 24 | 25 | func (r *noneMetrics) EmitLarkApiSuccess() {} 26 | 27 | func (r *noneMetrics) EmitAppSuccess() {} 28 | 29 | func (r *noneMetrics) EmitAppFailed() {} 30 | -------------------------------------------------------------------------------- /larkgpt/lark.go: -------------------------------------------------------------------------------- 1 | package larkgpt 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/chyroc/lark" 8 | ) 9 | 10 | type larkClient struct { 11 | cli *lark.Lark 12 | metricsIns IMetrics 13 | } 14 | 15 | func newLarkClient(cli *lark.Lark, metricsIns IMetrics) *larkClient { 16 | return &larkClient{ 17 | cli: cli, 18 | metricsIns: metricsIns, 19 | } 20 | } 21 | 22 | func (r *larkClient) replyText(ctx context.Context, msgID, text string) error { 23 | _, _, err := r.cli.Message.Reply(msgID).SendText(ctx, text) 24 | if err != nil { 25 | r.metricsIns.EmitLarkApiFailed() 26 | log.Println("LarkAPI 调用失败 请稍后重试. ", err) 27 | } else { 28 | r.metricsIns.EmitLarkApiSuccess() 29 | } 30 | return err 31 | } 32 | -------------------------------------------------------------------------------- /larkgpt/chatgpt_test.go: -------------------------------------------------------------------------------- 1 | package larkgpt 2 | 3 | import "testing" 4 | 5 | func TestChatGPTRequest(t *testing.T) { 6 | type args struct { 7 | msg string 8 | userID string 9 | } 10 | tests := []struct { 11 | name string 12 | args args 13 | wantErr bool 14 | }{ 15 | // TODO: Add test cases. 16 | { 17 | name: "test1", 18 | args: args{ 19 | msg: "Hey", 20 | userID: "", 21 | }, 22 | wantErr: false, 23 | }, 24 | } 25 | for _, tt := range tests { 26 | t.Run(tt.name, func(t *testing.T) { 27 | cli := newChatGPTClient("", "", new(noneMetrics)) 28 | _, err := cli.ChatGPTRequest(tt.args.msg, tt.args.userID) 29 | if (err != nil) != tt.wantErr { 30 | t.Errorf("ChatGPTRequest() error = %v, wantErr %v", err, tt.wantErr) 31 | return 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/go 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=go 3 | 4 | ### Go ### 5 | # If you prefer the allow list template instead of the deny list, see community template: 6 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 7 | # 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, built with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Dependency directories (remove the comment below to include it) 22 | # vendor/ 23 | 24 | # Go workspace file 25 | go.work 26 | 27 | # End of https://www.toptal.com/developers/gitignore/api/go 28 | n 29 | .env 30 | .idea/ 31 | ttt/ 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/bytemate/larkgpt/larkgpt" 8 | "github.com/joho/godotenv" 9 | ) 10 | 11 | func main() { 12 | config, err := loadConfig() 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | client := larkgpt.New(config) 17 | 18 | if err := client.Start(); err != nil { 19 | log.Fatal(err) 20 | } 21 | } 22 | 23 | func loadConfig() (*larkgpt.ClientConfig, error) { 24 | godotenv.Load(".env", "../.env") 25 | port := os.Getenv("PORT") 26 | if port == "" { 27 | port = "8080" 28 | } 29 | 30 | return &larkgpt.ClientConfig{ 31 | AppID: os.Getenv("APP_ID"), 32 | AppSecret: os.Getenv("APP_SECRET"), 33 | ChatGPTAPIKey: os.Getenv("CHATGPT_API_KEY"), 34 | ChatGPTAPIURL: os.Getenv("CHATGPT_API_URL"), 35 | ServerPort: port, 36 | Maintained: os.Getenv("MAINTAINED") == "true", 37 | EnableSessionForLarkGroup: os.Getenv("ENABLE_SESSION_FOR_LARK_GROUP") == "true", 38 | }, nil 39 | } 40 | -------------------------------------------------------------------------------- /larkgpt/client.go: -------------------------------------------------------------------------------- 1 | package larkgpt 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/chyroc/lark" 8 | ) 9 | 10 | type Client struct { 11 | larkIns *larkClient 12 | chatGPTIns *chatGPTClient 13 | metricsIns IMetrics 14 | serverPort string 15 | maintained bool 16 | enableSessionForLarkGroup bool 17 | } 18 | 19 | type ClientConfig struct { 20 | // Lark 21 | AppID string 22 | AppSecret string 23 | 24 | // ChatGPT 25 | ChatGPTAPIKey string 26 | ChatGPTAPIURL string 27 | Maintained bool 28 | 29 | // server 30 | ServerPort string 31 | Metrics IMetrics 32 | EnableSessionForLarkGroup bool // 给群聊的消息启动 session,session id 是消息的 root id 33 | } 34 | 35 | func New(config *ClientConfig) *Client { 36 | res := new(Client) 37 | 38 | res.metricsIns = config.Metrics 39 | if res.metricsIns == nil { 40 | res.metricsIns = new(noneMetrics) 41 | } 42 | 43 | res.larkIns = newLarkClient(lark.New(lark.WithAppCredential(config.AppID, config.AppSecret)), res.metricsIns) 44 | 45 | res.chatGPTIns = newChatGPTClient(config.ChatGPTAPIURL, config.ChatGPTAPIKey, res.metricsIns) 46 | 47 | res.serverPort = config.ServerPort 48 | 49 | res.maintained = config.Maintained 50 | res.enableSessionForLarkGroup = config.EnableSessionForLarkGroup 51 | 52 | return res 53 | } 54 | 55 | func (r *Client) Start() error { 56 | r.larkIns.cli.EventCallback.HandlerEventV2IMMessageReceiveV1(r.larkMessageReceiverHandler) 57 | 58 | http.HandleFunc("/event", func(w http.ResponseWriter, req *http.Request) { 59 | r.larkIns.cli.EventCallback.ListenCallback(req.Context(), req.Body, w) 60 | }) 61 | 62 | fmt.Printf("start server: %s ...\n", r.serverPort) 63 | return http.ListenAndServe(fmt.Sprint("[::]:", r.serverPort), nil) 64 | } 65 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= 2 | github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= 3 | github.com/chyroc/go-ptr v1.7.0 h1:wwm53+SeMvpQbBcsZviNPlz2djpxyELjVOZFHtUjHho= 4 | github.com/chyroc/go-ptr v1.7.0/go.mod h1:+AAlVrPed1XbNyqZ7r/J3o79V1awCvd5wrYkxu8tsow= 5 | github.com/chyroc/lark v0.0.104 h1:1mctdYeyO2aD8T4GR3M9fQYOik8XP4tEKXylQWJEIgI= 6 | github.com/chyroc/lark v0.0.104/go.mod h1:iYv0NXuCzYeypk1eY9Mos6QP7CQuLK7YH3Oxi09p7NU= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= 11 | github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= 12 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 13 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 18 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 19 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 21 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 22 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 23 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM= 24 | golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 25 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 26 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 28 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 29 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 33 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | -------------------------------------------------------------------------------- /larkgpt/larkbot.go: -------------------------------------------------------------------------------- 1 | package larkgpt 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "strings" 7 | 8 | "github.com/chyroc/lark" 9 | ) 10 | 11 | func isNonsense(content *lark.MessageContent, msg string) bool { 12 | if content != nil && content.Post != nil { 13 | for _, list := range content.Post.Content { 14 | for _, v := range list { 15 | if atPost, ok := v.(lark.MessageContentPostAt); ok { 16 | if atPost.UserID == larkAtAllText { 17 | // msg include @_all, ignore 18 | return false 19 | } 20 | } 21 | } 22 | } 23 | } 24 | 25 | // includee message 26 | // msg include @_all 27 | return strings.Contains(msg, larkAtAllText) || msg == "" 28 | } 29 | 30 | const larkAtAllText = "@_all" 31 | 32 | func filterMsg(msg string) string { 33 | // filter message 34 | // msg include @_user_1 35 | if strings.Contains(msg, "@_user_1") { 36 | msg = strings.ReplaceAll(msg, "@_user_1", "") 37 | } 38 | return msg 39 | } 40 | 41 | func (r *Client) ReceiveChatGPTMessage(ctx context.Context, msg string, event *lark.EventV2IMMessageReceiveV1) (err error) { 42 | defer func() { 43 | if err != nil { 44 | r.metricsIns.EmitAppFailed() 45 | } else { 46 | r.metricsIns.EmitAppSuccess() 47 | } 48 | }() 49 | 50 | log.Print("Receive message: ", msg) 51 | if r.maintained { 52 | return r.larkIns.replyText(context.Background(), event.Message.MessageID, "ChatGPT Bot 正在维护中 请稍后重试.请飞书搜索 ChatGPT 讨论群, 选择同款头像进群看进度.") 53 | } 54 | 55 | var result string 56 | sessionID := getLarkMessageSessionID(event, r.enableSessionForLarkGroup) 57 | if sessionID != "" { 58 | result, err = r.chatGPTIns.ChatGPTRequest(msg, sessionID) 59 | } else { 60 | result, err = r.chatGPTIns.ChatGPTOneTimeRequest(msg) 61 | } 62 | log.Println("msg: ", msg, "session", sessionID, "result: ", result) 63 | if err != nil { 64 | log.Println("ChatGPT 请求失败 请稍后重试. ", err) 65 | return r.larkIns.replyText(context.Background(), event.Message.MessageID, "ChatGPT 请求失败 请稍后重试.") 66 | } 67 | 68 | return r.larkIns.replyText(context.Background(), event.Message.MessageID, result) 69 | } 70 | 71 | func (r *Client) ReceiveCommandMessage(ctx context.Context, command string, event *lark.EventV2IMMessageReceiveV1) { 72 | switch command { 73 | case "/reset": 74 | sessionID := getLarkMessageSessionID(event, r.enableSessionForLarkGroup) 75 | var err error 76 | if sessionID != "" { 77 | err = r.chatGPTIns.DeleteSession(sessionID) 78 | } 79 | if err != nil { 80 | r.larkIns.replyText(context.Background(), event.Message.MessageID, "Reset Failed.") 81 | return 82 | } 83 | r.larkIns.replyText(context.Background(), event.Message.MessageID, "Reset Success.") 84 | default: 85 | r.larkIns.replyText(context.Background(), event.Message.MessageID, "Unknown Command.") 86 | } 87 | } 88 | 89 | func (r *Client) larkMessageReceiverHandler(ctx context.Context, cli *lark.Lark, schema string, header *lark.EventHeaderV2, event *lark.EventV2IMMessageReceiveV1) (string, error) { 90 | content, err := lark.UnwrapMessageContent(event.Message.MessageType, event.Message.Content) 91 | if err != nil { 92 | return "", err 93 | } 94 | msg := "" 95 | switch event.Message.MessageType { 96 | case lark.MsgTypeText: 97 | msg = content.Text.Text 98 | case lark.MsgTypePost: 99 | msg = wrapLarkPostMessageText(content) 100 | default: 101 | log.Println("暂不支持的消息类型.") 102 | _ = r.larkIns.replyText(context.Background(), event.Message.MessageID, "暂不支持的消息类型.") 103 | return "", nil 104 | } 105 | msg = filterMsg(msg) 106 | if isNonsense(content, msg) { 107 | return "", nil 108 | } 109 | switch true { 110 | case strings.HasPrefix(msg, "/"): 111 | go r.ReceiveCommandMessage(ctx, msg, event) 112 | default: 113 | go r.ReceiveChatGPTMessage(ctx, msg, event) 114 | } 115 | return "", err 116 | } 117 | 118 | func wrapLarkPostMessageText(content *lark.MessageContent) string { 119 | builder := new(strings.Builder) 120 | for idx, postContentList := range content.Post.Content { 121 | if idx != 0 { 122 | builder.WriteString("\n") 123 | } 124 | for _, postContent := range postContentList { 125 | switch postContent := postContent.(type) { 126 | case lark.MessageContentPostLink: 127 | builder.WriteString(postContent.Href) 128 | case lark.MessageContentPostText: 129 | builder.WriteString(postContent.Text) 130 | } 131 | } 132 | } 133 | return builder.String() 134 | } 135 | 136 | func getLarkMessageSessionID(event *lark.EventV2IMMessageReceiveV1, enableSessionForLarkGroup bool) string { 137 | if event.Message.ChatType == lark.ChatModeP2P { 138 | return event.Sender.SenderID.OpenID 139 | } 140 | if !enableSessionForLarkGroup { 141 | return "" 142 | } 143 | if event.Message.RootID == "" { 144 | // 自己就是根消息 145 | return event.Message.MessageID 146 | } 147 | return event.Message.RootID 148 | } 149 | -------------------------------------------------------------------------------- /larkgpt/chatgpt.go: -------------------------------------------------------------------------------- 1 | package larkgpt 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/go-resty/resty/v2" 8 | // retry 9 | "github.com/avast/retry-go" 10 | ) 11 | 12 | type chatGPTClient struct { 13 | apiHost string 14 | apiKey string 15 | metricsIns IMetrics 16 | } 17 | 18 | func newChatGPTClient(apiHost, apiKey string, metricsIns IMetrics) *chatGPTClient { 19 | return &chatGPTClient{ 20 | apiHost: apiHost, 21 | apiKey: apiKey, 22 | metricsIns: metricsIns, 23 | } 24 | } 25 | 26 | func (r *chatGPTClient) ChatGPTRequest(msg string, userID string) (result string, err error) { 27 | defer func() { 28 | if err != nil { 29 | r.metricsIns.EmitChatGPTApiFailed() 30 | } else { 31 | r.metricsIns.EmitChatGPTApiSuccess() 32 | } 33 | }() 34 | 35 | client := resty.New() 36 | resp, err := client.R(). 37 | SetHeaders( 38 | map[string]string{ 39 | "Content-Type": "application/json", 40 | "User-Agent": "LarkGPT", 41 | }). 42 | SetResult(&ChatGPTResponse{}). 43 | SetBody(map[string]string{ 44 | "message": msg, 45 | }). 46 | Post(r.apiHost + "message/" + userID) 47 | if resp.Size() == 0 { 48 | return "", errors.New("ChatGPT return empty response") 49 | } 50 | if err != nil { 51 | return "", err 52 | } 53 | if resp.StatusCode() == 429 { 54 | return "ChatGPT 访问过于频繁, 请稍后再试.", err 55 | } 56 | response := resp.Result().(*ChatGPTResponse) 57 | if response.Response == "" && resp.Size() == 0 { 58 | return "", err 59 | } 60 | if response != nil { 61 | return response.Response, nil 62 | } 63 | return "", err 64 | } 65 | 66 | func (r *chatGPTClient) ChatGPTOneTimeRequest(msg string) (result string, err error) { 67 | defer func() { 68 | if err != nil { 69 | r.metricsIns.EmitChatGPTApiFailed() 70 | } else { 71 | r.metricsIns.EmitChatGPTApiSuccess() 72 | } 73 | }() 74 | 75 | client := resty.New() 76 | var resp *resty.Response 77 | err = retry.Do( 78 | func() error { 79 | var err error 80 | resp, err = client.R(). 81 | SetHeaders( 82 | map[string]string{ 83 | "Content-Type": "application/json", 84 | "User-Agent": "LarkGPT", 85 | }). 86 | SetResult(&ChatGPTResponse{}). 87 | SetBody(map[string]string{ 88 | "message": msg, 89 | }). 90 | Post(r.apiHost + "message") 91 | if err != nil { 92 | return err 93 | } 94 | if resp.Size() == 0 { 95 | return errors.New("ChatGPT return empty response") 96 | } 97 | return nil 98 | }, 99 | retry.Attempts(3), 100 | retry.DelayType(retry.FixedDelay), 101 | // Delay 3 seconds 102 | retry.Delay(4*time.Second), 103 | ) 104 | if err != nil { 105 | return "", err 106 | } 107 | if resp.StatusCode() == 429 { 108 | return "ChatGPT 访问过于频繁, 请稍后再试.", err 109 | } 110 | response := resp.Result().(*ChatGPTResponse) 111 | if response.Response == "" && resp.Size() == 0 { 112 | return "", err 113 | } 114 | if response != nil { 115 | return response.Response, nil 116 | } 117 | return "", err 118 | } 119 | 120 | func (r *chatGPTClient) DeleteSession(userID string) (err error) { 121 | defer func() { 122 | if err != nil { 123 | r.metricsIns.EmitChatGPTApiFailed() 124 | } else { 125 | r.metricsIns.EmitChatGPTApiSuccess() 126 | } 127 | }() 128 | 129 | client := resty.New() 130 | resp, err := client.R(). 131 | SetHeaders( 132 | map[string]string{ 133 | "Content-Type": "application/json", 134 | "User-Agent": "LarkGPT", 135 | }). 136 | Delete(r.apiHost + "message/" + userID) 137 | if err != nil { 138 | return err 139 | } 140 | if resp.StatusCode() == 429 { 141 | return err 142 | } 143 | return nil 144 | } 145 | 146 | // {"response":"Here is an example of CSS animation that changes the background color from red to yellow over a 4-second period, with the first 3 seconds being red and the last 1 second being yellow:\n\n```css\n@keyframes colorChange {\n 0% { background-color: red; }\n 75% { background-color: red; }\n 100% { background-color: yellow; }\n}\n\n#myElement {\n animation: colorChange 4s;\n}\n```\n\nThis animation is applied to an element with the id \"myElement\", and the animation is called \"colorChange\". The animation lasts for 4 seconds, and the keyframe percentages specify the progression of the animation. At 0% (the start of the animation), the background color is set to red. At 75%, the background color is still red. At 100% (the end of the animation), the background color is set to yellow.\n\nYou can also adjust the timing function for more fluent animation, Like\n```\n#myElement {\n animation: colorChange 4s ease-in-out;\n}\n```\n\n`ease-in-out` is one of the timing function, this will make the animation start slow and end slow as well.\n","conversationId":"fe46dbb2-9074-4364-8358-abaf08535e5c","messageId":"af3dcb0e-0827-42cc-85fe-b1f9307110f7"} 147 | type ChatGPTResponse struct { 148 | Response string `json:"response"` 149 | ConversationID string `json:"conversationId"` 150 | MessageID string `json:"messageId"` 151 | } 152 | --------------------------------------------------------------------------------