优化 ChatGPT API 重试逻辑

This commit is contained in:
RockYang 2023-03-20 15:02:42 +08:00
parent 0ca104bac8
commit 3bb6814493
9 changed files with 167 additions and 113 deletions

View File

@ -6,5 +6,9 @@
* [ ] 使用 level DB 保存用户聊天的上下文 * [ ] 使用 level DB 保存用户聊天的上下文
* [ ] 使用 MySQL 保存用户的聊天的历史记录 * [ ] 使用 MySQL 保存用户的聊天的历史记录
* [ ] 用户聊天鉴权 * [ ] 用户聊天鉴权,设置口令模式
* [ ] 每次连接自动加载历史记录
* [ ] 角色设定,预设一些角色,比如程序员,产品经理,医生,作家,老师...
* [ ] markdown 语法解析
* [ ] 用户配置界面

View File

@ -10,7 +10,6 @@ import (
"io" "io"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url"
"openai/types" "openai/types"
"strings" "strings"
"time" "time"
@ -74,34 +73,32 @@ func (s *Server) sendMessage(userId string, text string, ws Client) error {
return err return err
} }
// TODO: API KEY 负载均衡
rand.Seed(time.Now().UnixNano())
index := rand.Intn(len(s.Config.Chat.ApiKeys))
logger.Infof("Use API KEY: %s", s.Config.Chat.ApiKeys[index])
request.Header.Add("Content-Type", "application/json") request.Header.Add("Content-Type", "application/json")
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", s.Config.Chat.ApiKeys[index])) // 随机获取一个 API Key如果请求失败则更换 API Key 重试
// TODO: 需要将失败的 Key 移除列表
uri := url.URL{} rand.Seed(time.Now().UnixNano())
proxy, _ := uri.Parse(s.Config.ProxyURL)
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
}
response, err := client.Do(request)
var retryCount = 3 var retryCount = 3
for err != nil { var response *http.Response
if retryCount <= 0 { for retryCount > 0 {
return err index := rand.Intn(len(s.Config.Chat.ApiKeys))
apiKey := s.Config.Chat.ApiKeys[index]
logger.Infof("Use API KEY: %s", apiKey)
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
response, err = s.Client.Do(request)
if err == nil {
break
} else {
logger.Error(err)
} }
response, err = client.Do(request)
retryCount-- retryCount--
} }
if err != nil {
return err
}
var message = types.Message{} var message = types.Message{}
var contents = make([]string, 0) var contents = make([]string, 0)
var responseBody = types.ApiResponse{} var responseBody = types.ApiResponse{}
reader := bufio.NewReader(response.Body) reader := bufio.NewReader(response.Body)
for { for {
line, err := reader.ReadString('\n') line, err := reader.ReadString('\n')
@ -119,7 +116,7 @@ func (s *Server) sendMessage(userId string, text string, ws Client) error {
err = json.Unmarshal([]byte(line[6:]), &responseBody) err = json.Unmarshal([]byte(line[6:]), &responseBody)
if err != nil { if err != nil {
logger.Error(err) logger.Error(line)
continue continue
} }
// 初始化 role // 初始化 role

View File

@ -73,6 +73,11 @@ func (s *Server) ConfigSetHandle(c *gin.Context) {
// 保存配置文件 // 保存配置文件
logger.Infof("Config: %+v", s.Config) logger.Infof("Config: %+v", s.Config)
types.SaveConfig(s.Config, s.ConfigPath) err = types.SaveConfig(s.Config, s.ConfigPath)
if err != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Failed to save config file"})
return
}
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg}) c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Message: types.OkMsg})
} }

View File

@ -9,6 +9,7 @@ import (
"io/fs" "io/fs"
"log" "log"
"net/http" "net/http"
"net/url"
logger2 "openai/logger" logger2 "openai/logger"
"openai/types" "openai/types"
"os" "os"
@ -32,6 +33,7 @@ func (s StaticFile) Open(name string) (fs.File, error) {
type Server struct { type Server struct {
Config *types.Config Config *types.Config
ConfigPath string ConfigPath string
Client *http.Client
History map[string][]types.Message History map[string][]types.Message
} }
@ -41,8 +43,17 @@ func NewServer(configPath string) (*Server, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
uri := url.URL{}
proxy, _ := uri.Parse(config.ProxyURL)
client := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
}
return &Server{ return &Server{
Config: config, Config: config,
Client: client,
ConfigPath: configPath, ConfigPath: configPath,
History: make(map[string][]types.Message, 16)}, nil History: make(map[string][]types.Message, 16)}, nil
} }

View File

@ -1 +1 @@
VUE_APP_WS_HOST=ws://127.0.0.1:5678 VUE_APP_WS_HOST=ws://172.22.11.200:5678

View File

@ -34,7 +34,7 @@ export default defineComponent({
<style lang="stylus"> <style lang="stylus">
.chat-line-right { .chat-line-right {
justify-content: end; justify-content: flex-end;
.chat-icon { .chat-icon {
margin-left 5px; margin-left 5px;

View File

@ -6,10 +6,7 @@
<div class="chat-item"> <div class="chat-item">
<div class="triangle"></div> <div class="triangle"></div>
<div class="content"> <div class="content" v-html="content"></div>
<span v-html="content"></span>
<span class="cursor" v-show="cursor"></span>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -41,7 +38,7 @@ export default defineComponent({
<style lang="stylus"> <style lang="stylus">
.chat-line-left { .chat-line-left {
justify-content: start; justify-content: flex-start;
.chat-icon { .chat-icon {
margin-right 5px; margin-right 5px;
@ -76,20 +73,20 @@ export default defineComponent({
font-size: var(--content-font-size); font-size: var(--content-font-size);
border-radius: 5px; border-radius: 5px;
.cursor { //.cursor {
height 24px; // height 24px;
border-left 1px solid black; // border-left 1px solid black;
//
animation: cursorImg 1s infinite steps(1, start); // animation: cursorImg 1s infinite steps(1, start);
@keyframes cursorImg { // @keyframes cursorImg {
0%, 100% { // 0%, 100% {
opacity: 0; // opacity: 0;
} // }
50% { // 50% {
opacity: 1; // opacity: 1;
} // }
} // }
} //}
} }
} }
} }

View File

@ -2,7 +2,6 @@
<el-dialog <el-dialog
v-model="$props.show" v-model="$props.show"
title="聊天配置" title="聊天配置"
width="30%"
:before-close="beforeClose" :before-close="beforeClose"
> >
<span>正在努力开发中...</span> <span>正在努力开发中...</span>
@ -42,5 +41,8 @@ export default defineComponent({
</script> </script>
<style lang="stylus"> <style lang="stylus">
.el-dialog {
--el-dialog-width 90%;
max-width 800px;
}
</style> </style>

View File

@ -17,16 +17,16 @@
<div class="input-box"> <div class="input-box">
<div class="input-container"> <div class="input-container">
<el-form ref="form"> <el-input
<el-input ref="text-input"
v-model="inputValue" v-model="inputValue"
:autosize="{ minRows: 1, maxRows: 10 }" :autosize="{ minRows: 1, maxRows: 10 }"
v-on:keydown="inputKeyDown" v-on:keydown="inputKeyDown"
v-on:focus="focus" v-on:focus="focus"
type="textarea" autofocus
placeholder="Input any thing here..." type="textarea"
/> placeholder="Input any thing here..."
</el-form> />
</div> </div>
<div class="btn-container"> <div class="btn-container">
@ -41,6 +41,7 @@
</div> </div>
</div><!-- end input box --> </div><!-- end input box -->
</div><!-- end container --> </div><!-- end container -->
<config-dialog v-model:show="showDialog"></config-dialog> <config-dialog v-model:show="showDialog"></config-dialog>
@ -52,7 +53,7 @@ import {defineComponent, nextTick} from 'vue'
import ChatPrompt from "@/components/ChatPrompt.vue"; import ChatPrompt from "@/components/ChatPrompt.vue";
import ChatReply from "@/components/ChatReply.vue"; import ChatReply from "@/components/ChatReply.vue";
import {randString} from "@/utils/libs"; import {randString} from "@/utils/libs";
import {ElMessage} from 'element-plus' import {ElMessage, ElMessageBox} from 'element-plus'
import {Tools} from '@element-plus/icons-vue' import {Tools} from '@element-plus/icons-vue'
import ConfigDialog from '@/components/ConfigDialog.vue' import ConfigDialog from '@/components/ConfigDialog.vue'
@ -67,6 +68,7 @@ export default defineComponent({
chatBoxHeight: 0, chatBoxHeight: 0,
showDialog: false, showDialog: false,
connectingMessageBox: null,
socket: null, socket: null,
sending: false sending: false
} }
@ -78,53 +80,84 @@ export default defineComponent({
nextTick(() => { nextTick(() => {
this.chatBoxHeight = window.innerHeight - 61; this.chatBoxHeight = window.innerHeight - 61;
}) })
this.connect();
// WebSocket
const socket = new WebSocket(process.env.VUE_APP_WS_HOST + '/api/chat');
socket.addEventListener('open', () => {
ElMessage.success('创建会话成功!');
});
socket.addEventListener('message', event => {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
const data = JSON.parse(String(reader.result));
if (data.type === 'start') {
this.chatData.push({
type: "reply",
id: randString(32),
icon: 'images/gpt-icon.png',
content: "",
cursor: true
});
} else if (data.type === 'end') {
this.sending = false;
this.chatData[this.chatData.length - 1]["cursor"] = false;
} else {
let content = data.content;
if (content.indexOf("\n\n") >= 0) {
content = content.replace("\n\n", "<br />");
}
this.chatData[this.chatData.length - 1]["content"] += content;
}
//
nextTick(() => {
document.getElementById('container').scrollTo(0, document.getElementById('container').scrollHeight)
})
};
}
});
socket.addEventListener('error', () => {
ElMessage.error('会话发生异常,请刷新页面后重试');
});
this.socket = socket;
}, },
methods: { methods: {
connect: function () {
if (this.online) {
return
}
// WebSocket
const socket = new WebSocket(process.env.VUE_APP_WS_HOST + '/api/chat');
socket.addEventListener('open', () => {
ElMessage.success('创建会话成功!');
if (this.connectingMessageBox != null) {
this.connectingMessageBox.close();
this.connectingMessageBox = null;
}
});
socket.addEventListener('message', event => {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
const data = JSON.parse(String(reader.result));
if (data.type === 'start') {
this.chatData.push({
type: "reply",
id: randString(32),
icon: 'images/gpt-icon.png',
content: "",
cursor: true
});
} else if (data.type === 'end') {
this.sending = false;
this.chatData[this.chatData.length - 1]["cursor"] = false;
} else {
let content = data.content;
//
if (content.indexOf("\n\n") >= 0) {
content = content.replace("\n\n", "<br />");
}
this.chatData[this.chatData.length - 1]["content"] += content;
}
//
nextTick(() => {
document.getElementById('container').scrollTo(0, document.getElementById('container').scrollHeight)
})
};
}
});
socket.addEventListener('close', () => {
ElMessageBox.confirm(
'^_^ 会话发生异常,您已经从服务器断开连接!',
'注意:',
{
confirmButtonText: '重连会话',
cancelButtonText: '不聊了',
type: 'warning',
}
)
.then(() => {
this.connect();
})
.catch(() => {
ElMessage({
type: 'info',
message: '您关闭了会话',
})
})
});
this.socket = socket;
},
inputKeyDown: function (e) { inputKeyDown: function (e) {
if (e.keyCode === 13) { if (e.keyCode === 13) {
if (this.sending) { if (this.sending) {
@ -152,7 +185,10 @@ export default defineComponent({
// TODO: 使 websocket // TODO: 使 websocket
this.sending = true; this.sending = true;
this.socket.send(this.inputValue); this.socket.send(this.inputValue);
this.$refs["text-input"].blur();
this.inputValue = ''; this.inputValue = '';
// textarea
setTimeout(() => this.$refs["text-input"].focus(), 100)
return true; return true;
}, },
@ -227,20 +263,12 @@ export default defineComponent({
background-color: rgba(255, 255, 255, 1); background-color: rgba(255, 255, 255, 1);
padding: 5px 10px; padding: 5px 10px;
.input-text { .el-textarea__inner {
font-size: 16px; box-shadow: none
padding 0 padding 5px 0
margin 0
outline: none;
width 100%;
border none
background #ffffff
resize none
line-height 24px;
color #333;
} }
.input-text::-webkit-scrollbar { .el-textarea__inner::-webkit-scrollbar {
width: 0; width: 0;
height: 0; height: 0;
} }
@ -267,9 +295,19 @@ export default defineComponent({
width: 0; width: 0;
height: 0; height: 0;
} }
} }
} }
.el-message-box {
width 90%;
max-width 420px;
}
.el-message {
width 90%;
min-width: 300px;
max-width 600px;
}
</style> </style>