diff --git a/api/anonymous.go b/api/anonymous.go index f706300..8abc747 100644 --- a/api/anonymous.go +++ b/api/anonymous.go @@ -16,39 +16,50 @@ type AnonymousRequestBody struct { Message string `json:"message" required:"true"` } -func GetAnonymousResponse(message string) (string, error) { +type AnonymousResponseCache struct { + Keyword string `json:"keyword"` + Message string `json:"message"` +} + +func GetChatGPTResponse(message []types.ChatGPTMessage, token int) (string, error) { res, err := utils.Post(viper.GetString("openai.anonymous_endpoint")+"/chat/completions", map[string]string{ "Content-Type": "application/json", - "Authorization": "Bearer " + viper.GetString("openai.anonymous"), + "Authorization": "Bearer " + GetRandomKey(viper.GetString("openai.anonymous")), }, types.ChatGPTRequest{ - Model: "gpt-3.5-turbo", - Messages: ChatWithWeb([]types.ChatGPTMessage{ - { - Role: "user", - Content: message, - }, - }, message), - MaxToken: 1000, + Model: "gpt-3.5-turbo", + Messages: message, + MaxToken: token, }) - if err != nil { + if err != nil || res == nil { return "", err } data := res.(map[string]interface{})["choices"].([]interface{})[0].(map[string]interface{})["message"].(map[string]interface{})["content"] return data.(string), nil } -func GetAnonymousResponseWithCache(c *gin.Context, message string) (string, error) { +func GetAnonymousResponse(message string) (string, string, error) { + keyword, source := ChatWithWeb([]types.ChatGPTMessage{{Role: "user", Content: message}}) + resp, err := GetChatGPTResponse(source, 1000) + return keyword, resp, err +} + +func GetAnonymousResponseWithCache(c *gin.Context, message string) (string, string, error) { cache := c.MustGet("cache").(*redis.Client) res, err := cache.Get(c, fmt.Sprintf(":chatgpt:%s", message)).Result() - if err != nil || len(res) == 0 { - res, err := GetAnonymousResponse(message) + form := utils.UnmarshalJson[AnonymousResponseCache](res) + if err != nil || len(res) == 0 || res == "{}" || form.Message == "" { + key, res, err := GetAnonymousResponse(message) if err != nil { - return "There was something wrong...", err + return "", "There was something wrong...", err } - cache.Set(c, fmt.Sprintf(":chatgpt:%s", message), res, time.Hour*6) - return res, nil + + cache.Set(c, fmt.Sprintf(":chatgpt:%s", message), utils.ToJson(AnonymousResponseCache{ + Keyword: key, + Message: res, + }), time.Hour*6) + return key, res, nil } - return res, nil + return form.Keyword, form.Message, nil } func AnonymousAPI(c *gin.Context) { @@ -57,6 +68,7 @@ func AnonymousAPI(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": false, "message": "", + "keyword": "", "reason": err.Error(), }) return @@ -66,15 +78,17 @@ func AnonymousAPI(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": false, "message": "", + "keyword": "", "reason": "message is empty", }) return } - res, err := GetAnonymousResponseWithCache(c, message) + key, res, err := GetAnonymousResponseWithCache(c, message) if err != nil { c.JSON(http.StatusOK, gin.H{ "status": false, "message": res, + "keyword": key, "reason": err.Error(), }) return @@ -82,6 +96,7 @@ func AnonymousAPI(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": true, "message": res, + "keyword": key, "reason": "", }) } diff --git a/api/chat.go b/api/chat.go index 27fc78b..dd3ce12 100644 --- a/api/chat.go +++ b/api/chat.go @@ -63,9 +63,11 @@ func ChatAPI(c *gin.Context) { if err != nil { return } - if msg, err := instance.AddMessageFromUserForm(message); err == nil { - StreamRequest("gpt-3.5-turbo", ChatWithWeb(instance.GetMessageSegment(5), msg), 1500, func(resp string) { + if _, err := instance.AddMessageFromUserForm(message); err == nil { + keyword, segment := ChatWithWeb(instance.GetMessageSegment(12)) + StreamRequest("gpt-3.5-turbo-16k", segment, 1500, func(resp string) { data, _ := json.Marshal(map[string]interface{}{ + "keyword": keyword, "message": resp, "end": false, }) diff --git a/api/stream.go b/api/stream.go index 23dd768..f892a64 100644 --- a/api/stream.go +++ b/api/stream.go @@ -30,12 +30,17 @@ func processLine(buf []byte) []string { item = item + "}" } - if item == "{data: [DONE]}" { + if item == "{data: [DONE]}" || item == "{data: [DONE]}}" || item == "{[DONE]}" { break + } else if item == "{data:}" || item == "{data:}}" { + continue } + var form types.ChatGPTStreamResponse if err := json.Unmarshal([]byte(item), &form); err != nil { - log.Println(item, err) + if err := json.Unmarshal([]byte(item[:len(item)-1]), &form); err != nil { + log.Println(item, err) + } } choices := form.Data.Choices if len(choices) > 0 { @@ -60,7 +65,7 @@ func StreamRequest(model string, messages []types.ChatGPTMessage, token int, cal } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+viper.GetString("openai.user")) + req.Header.Set("Authorization", "Bearer "+GetRandomKey(viper.GetString("openai.user"))) res, err := client.Do(req) if err != nil { diff --git a/api/utils.go b/api/utils.go index 2987063..ee2cea2 100644 --- a/api/utils.go +++ b/api/utils.go @@ -5,12 +5,55 @@ import ( "chat/utils" "chat/web" "fmt" + "math/rand" + "strings" "time" ) -func ChatWithWeb(message []types.ChatGPTMessage, current string) []types.ChatGPTMessage { - return utils.Insert(message, 0, types.ChatGPTMessage{ - Role: "system", - Content: fmt.Sprintf("你将扮演AI问答助手,你的知识库不是截止于2021年9月,而是可以实时联网的,你可以提供实时联网的知识库。当前时间: %s, 你的知识库:%s", time.Now().Format("2006-01-02 15:04:05"), web.SearchBing(current)), +func ChatWithWeb(message []types.ChatGPTMessage) (string, []types.ChatGPTMessage) { + keyword := SearchWeb(message) + return keyword, utils.Insert(message, 0, types.ChatGPTMessage{ + Role: "system", + Content: fmt.Sprintf("你将扮演AI问答助手,你的知识库不是截止于2021年9月,而是可以实时联网的,你可以提供实时联网的知识库。"+ + "官网网站使用链接包裹,给予用户精确的答复。"+ + "当前时间: %s, 你的知识库:%s", + time.Now().Format("2006-01-02 15:04:05"), web.SearchBing(keyword), + ), }) } + +func GetRandomKey(apikey string) string { + arr := strings.Split(apikey, "|") + idx := rand.Intn(len(arr)) + return arr[idx] +} + +func StringCleaner(content string) string { + for _, replacer := range []string{",", "、", ",", "。", ":", ":", ";", ";", "!", "!", "?", "?", "(", ")", "(", ")"} { + content = strings.ReplaceAll(content, replacer, " ") + } + return content +} + +func SearchWeb(message []types.ChatGPTMessage) string { + source := make([]string, 0) + for _, item := range message { + if item.Role == "user" && item.Content != "" { + source = append(source, item.Content) + } + } + if len(source) == 0 { + return "" + } + + keyword, _ := GetChatGPTResponse([]types.ChatGPTMessage{{ + Role: "system", + Content: "在接下来的对话中,不要回答问题,你需要总结接下来的对话中出现的内容," + + "然后用关键字和空格分词,输出精简,请不要输出冗余内容," + + "不能出现逗号顿号等特殊字符,仅回答出现的关键词。", + }, { + Role: "user", + Content: strings.Join(source, " "), + }}, 40) + return StringCleaner(keyword) +} diff --git a/app/src/assets/script/conversation.ts b/app/src/assets/script/conversation.ts index c577af1..6caf840 100644 --- a/app/src/assets/script/conversation.ts +++ b/app/src/assets/script/conversation.ts @@ -10,10 +10,12 @@ type Message = { role: string; time: string; stamp: number; + keyword?: string; gpt4?: boolean; } type StreamMessage = { + keyword?: string; message: string; end: boolean; } @@ -102,16 +104,17 @@ export class Conversation { public async sendAuthenticated(content: string): Promise { this.state.value = true; this.addMessageFromUser(content); - let message = ref(""), end = ref(false); + let message = ref(""), end = ref(false), keyword = ref(""); this.connection?.setCallback((res: StreamMessage) => { message.value += res.message; + res.keyword && (keyword.value = res.keyword); end.value = res.end; }) const status = this.connection?.send({ message: content, }); if (status) { - this.addDynamicMessageFromAI(message, end); + this.addDynamicMessageFromAI(message, keyword, end); } else { this.addMessageFromAI("网络错误,请稍后再试"); } @@ -152,9 +155,10 @@ export class Conversation { }).then(r => 0); } - public addMessageFromAI(content: string): void { + public addMessageFromAI(content: string, keyword?: string): void { this.addMessage({ content: "", + keyword: keyword ? keyword : "", role: "bot", time: new Date().toLocaleTimeString(), stamp: new Date().getTime(), @@ -163,15 +167,16 @@ export class Conversation { this.typingEffect(this.len.value - 1, content); } - public addDynamicMessageFromAI(content: Ref, end: Ref): void { + public addDynamicMessageFromAI(content: Ref, keyword: Ref, end: Ref): void { this.addMessage({ content: "", role: "bot", + keyword: keyword.value || "", time: new Date().toLocaleTimeString(), stamp: new Date().getTime(), gpt4: gpt4.value, }) - this.dynamicTypingEffect(this.len.value - 1, content, end); + this.dynamicTypingEffect(this.len.value - 1, content, keyword, end); } public typingEffect(index: number, content: string): void { @@ -187,9 +192,10 @@ export class Conversation { }, 20); } - public dynamicTypingEffect(index: number, content: Ref, end: Ref): void { + public dynamicTypingEffect(index: number, content: Ref, keyword: Ref, end: Ref): void { let cursor = 0; const interval = setInterval(() => { + keyword.value && (this.messages[index].keyword = keyword.value); if (end.value && cursor >= content.value.length) { this.messages[index].content = content.value; this.state.value = false; diff --git a/app/src/assets/style/anim.css b/app/src/assets/style/anim.css index ac99162..795cf51 100644 --- a/app/src/assets/style/anim.css +++ b/app/src/assets/style/anim.css @@ -39,3 +39,12 @@ transform: rotate(359deg); } } + +@keyframes FadeInAnimation { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/app/src/components/icons/bing.vue b/app/src/components/icons/bing.vue new file mode 100644 index 0000000..bebcefa --- /dev/null +++ b/app/src/components/icons/bing.vue @@ -0,0 +1,24 @@ + diff --git a/app/src/views/HomeView.vue b/app/src/views/HomeView.vue index 4fcdbd7..55b490c 100644 --- a/app/src/views/HomeView.vue +++ b/app/src/views/HomeView.vue @@ -7,6 +7,7 @@ import {Conversation} from "../assets/script/conversation"; import {nextTick, onMounted, ref} from "vue"; import {auth, username} from "../assets/script/auth"; import Loading from "../components/icons/loading.vue"; +import Bing from "../components/icons/bing.vue"; const conversation = new Conversation(1, refreshScrollbar); const state = conversation.getState(), length = conversation.getLength(), messages = conversation.getMessages(); @@ -57,6 +58,10 @@ onMounted(() => {
+
+ + {{ message.keyword }} +
{{ message.content }} @@ -179,6 +184,25 @@ onMounted(() => { transform: translateY(10px); } +.bing { + display: inline-block; + color: #2f7eee; + background: #e8f2ff; + border-radius: 12px; + padding: 6px 12px; + font-size: 16px; + margin: 4px 0; + user-select: none; + animation: FadeInAnimation .5s cubic-bezier(0.18, 0.89, 0.32, 1.28) both; +} + +.bing svg { + width: 20px; + height: 20px; + margin-right: 6px; + transform: translate(3px, 3px); +} + .input .button:hover { cursor: pointer; } diff --git a/utils/base.go b/utils/base.go index b4d571b..337434a 100644 --- a/utils/base.go +++ b/utils/base.go @@ -1,6 +1,9 @@ package utils -import "fmt" +import ( + "encoding/json" + "fmt" +) func Contains[T comparable](value T, slice []T) bool { for _, item := range slice { @@ -29,3 +32,35 @@ func Insert[T any](arr []T, index int, value T) []T { arr[index] = value return arr } + +func InsertSlice[T any](arr []T, index int, value []T) []T { + arr = append(arr, value...) + copy(arr[index+len(value):], arr[index:]) + copy(arr[index:], value) + return arr +} + +func Remove[T any](arr []T, index int) []T { + return append(arr[:index], arr[index+1:]...) +} + +func RemoveSlice[T any](arr []T, index int, length int) []T { + return append(arr[:index], arr[index+length:]...) +} + +func ToJson(value interface{}) string { + if res, err := json.Marshal(value); err == nil { + return string(res) + } else { + return "{}" + } +} + +func UnmarshalJson[T any](value string) T { + var res T + if err := json.Unmarshal([]byte(value), &res); err == nil { + return res + } else { + return res + } +}