mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-09-17 16:56:38 +08:00
优化 ChatGPT API 重试逻辑
This commit is contained in:
parent
0ca104bac8
commit
3bb6814493
@ -6,5 +6,9 @@
|
||||
|
||||
* [ ] 使用 level DB 保存用户聊天的上下文
|
||||
* [ ] 使用 MySQL 保存用户的聊天的历史记录
|
||||
* [ ] 用户聊天鉴权
|
||||
* [ ] 用户聊天鉴权,设置口令模式
|
||||
* [ ] 每次连接自动加载历史记录
|
||||
* [ ] 角色设定,预设一些角色,比如程序员,产品经理,医生,作家,老师...
|
||||
* [ ] markdown 语法解析
|
||||
* [ ] 用户配置界面
|
||||
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"openai/types"
|
||||
"strings"
|
||||
"time"
|
||||
@ -74,34 +73,32 @@ func (s *Server) sendMessage(userId string, text string, ws Client) error {
|
||||
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("Authorization", fmt.Sprintf("Bearer %s", s.Config.Chat.ApiKeys[index]))
|
||||
|
||||
uri := url.URL{}
|
||||
proxy, _ := uri.Parse(s.Config.ProxyURL)
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxy),
|
||||
},
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
// 随机获取一个 API Key,如果请求失败,则更换 API Key 重试
|
||||
// TODO: 需要将失败的 Key 移除列表
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
var retryCount = 3
|
||||
for err != nil {
|
||||
if retryCount <= 0 {
|
||||
return err
|
||||
var response *http.Response
|
||||
for retryCount > 0 {
|
||||
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--
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var message = types.Message{}
|
||||
var contents = make([]string, 0)
|
||||
var responseBody = types.ApiResponse{}
|
||||
|
||||
reader := bufio.NewReader(response.Body)
|
||||
for {
|
||||
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)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
logger.Error(line)
|
||||
continue
|
||||
}
|
||||
// 初始化 role
|
||||
|
@ -73,6 +73,11 @@ func (s *Server) ConfigSetHandle(c *gin.Context) {
|
||||
|
||||
// 保存配置文件
|
||||
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})
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
logger2 "openai/logger"
|
||||
"openai/types"
|
||||
"os"
|
||||
@ -32,6 +33,7 @@ func (s StaticFile) Open(name string) (fs.File, error) {
|
||||
type Server struct {
|
||||
Config *types.Config
|
||||
ConfigPath string
|
||||
Client *http.Client
|
||||
History map[string][]types.Message
|
||||
}
|
||||
|
||||
@ -41,8 +43,17 @@ func NewServer(configPath string) (*Server, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uri := url.URL{}
|
||||
proxy, _ := uri.Parse(config.ProxyURL)
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxy),
|
||||
},
|
||||
}
|
||||
return &Server{
|
||||
Config: config,
|
||||
Client: client,
|
||||
ConfigPath: configPath,
|
||||
History: make(map[string][]types.Message, 16)}, nil
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
VUE_APP_WS_HOST=ws://127.0.0.1:5678
|
||||
VUE_APP_WS_HOST=ws://172.22.11.200:5678
|
@ -34,7 +34,7 @@ export default defineComponent({
|
||||
|
||||
<style lang="stylus">
|
||||
.chat-line-right {
|
||||
justify-content: end;
|
||||
justify-content: flex-end;
|
||||
|
||||
.chat-icon {
|
||||
margin-left 5px;
|
||||
|
@ -6,10 +6,7 @@
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="triangle"></div>
|
||||
<div class="content">
|
||||
<span v-html="content"></span>
|
||||
<span class="cursor" v-show="cursor"></span>
|
||||
</div>
|
||||
<div class="content" v-html="content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -41,7 +38,7 @@ export default defineComponent({
|
||||
|
||||
<style lang="stylus">
|
||||
.chat-line-left {
|
||||
justify-content: start;
|
||||
justify-content: flex-start;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 5px;
|
||||
@ -76,20 +73,20 @@ export default defineComponent({
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
|
||||
.cursor {
|
||||
height 24px;
|
||||
border-left 1px solid black;
|
||||
|
||||
animation: cursorImg 1s infinite steps(1, start);
|
||||
@keyframes cursorImg {
|
||||
0%, 100% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
//.cursor {
|
||||
// height 24px;
|
||||
// border-left 1px solid black;
|
||||
//
|
||||
// animation: cursorImg 1s infinite steps(1, start);
|
||||
// @keyframes cursorImg {
|
||||
// 0%, 100% {
|
||||
// opacity: 0;
|
||||
// }
|
||||
// 50% {
|
||||
// opacity: 1;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
<el-dialog
|
||||
v-model="$props.show"
|
||||
title="聊天配置"
|
||||
width="30%"
|
||||
:before-close="beforeClose"
|
||||
>
|
||||
<span>正在努力开发中...</span>
|
||||
@ -42,5 +41,8 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
.el-dialog {
|
||||
--el-dialog-width 90%;
|
||||
max-width 800px;
|
||||
}
|
||||
</style>
|
@ -17,16 +17,16 @@
|
||||
|
||||
<div class="input-box">
|
||||
<div class="input-container">
|
||||
<el-form ref="form">
|
||||
<el-input
|
||||
v-model="inputValue"
|
||||
:autosize="{ minRows: 1, maxRows: 10 }"
|
||||
v-on:keydown="inputKeyDown"
|
||||
v-on:focus="focus"
|
||||
type="textarea"
|
||||
placeholder="Input any thing here..."
|
||||
/>
|
||||
</el-form>
|
||||
<el-input
|
||||
ref="text-input"
|
||||
v-model="inputValue"
|
||||
:autosize="{ minRows: 1, maxRows: 10 }"
|
||||
v-on:keydown="inputKeyDown"
|
||||
v-on:focus="focus"
|
||||
autofocus
|
||||
type="textarea"
|
||||
placeholder="Input any thing here..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="btn-container">
|
||||
@ -41,6 +41,7 @@
|
||||
</div>
|
||||
|
||||
</div><!-- end input box -->
|
||||
|
||||
</div><!-- end container -->
|
||||
|
||||
<config-dialog v-model:show="showDialog"></config-dialog>
|
||||
@ -52,7 +53,7 @@ import {defineComponent, nextTick} from 'vue'
|
||||
import ChatPrompt from "@/components/ChatPrompt.vue";
|
||||
import ChatReply from "@/components/ChatReply.vue";
|
||||
import {randString} from "@/utils/libs";
|
||||
import {ElMessage} from 'element-plus'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {Tools} from '@element-plus/icons-vue'
|
||||
import ConfigDialog from '@/components/ConfigDialog.vue'
|
||||
|
||||
@ -67,6 +68,7 @@ export default defineComponent({
|
||||
chatBoxHeight: 0,
|
||||
showDialog: false,
|
||||
|
||||
connectingMessageBox: null,
|
||||
socket: null,
|
||||
sending: false
|
||||
}
|
||||
@ -78,53 +80,84 @@ export default defineComponent({
|
||||
nextTick(() => {
|
||||
this.chatBoxHeight = window.innerHeight - 61;
|
||||
})
|
||||
|
||||
// 初始化 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;
|
||||
this.connect();
|
||||
|
||||
},
|
||||
|
||||
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) {
|
||||
if (e.keyCode === 13) {
|
||||
if (this.sending) {
|
||||
@ -152,7 +185,10 @@ export default defineComponent({
|
||||
// TODO: 使用 websocket 提交数据到后端
|
||||
this.sending = true;
|
||||
this.socket.send(this.inputValue);
|
||||
this.$refs["text-input"].blur();
|
||||
this.inputValue = '';
|
||||
// 等待 textarea 重新调整尺寸之后再自动获取焦点
|
||||
setTimeout(() => this.$refs["text-input"].focus(), 100)
|
||||
return true;
|
||||
},
|
||||
|
||||
@ -227,20 +263,12 @@ export default defineComponent({
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
padding: 5px 10px;
|
||||
|
||||
.input-text {
|
||||
font-size: 16px;
|
||||
padding 0
|
||||
margin 0
|
||||
outline: none;
|
||||
width 100%;
|
||||
border none
|
||||
background #ffffff
|
||||
resize none
|
||||
line-height 24px;
|
||||
color #333;
|
||||
.el-textarea__inner {
|
||||
box-shadow: none
|
||||
padding 5px 0
|
||||
}
|
||||
|
||||
.input-text::-webkit-scrollbar {
|
||||
.el-textarea__inner::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
@ -267,9 +295,19 @@ export default defineComponent({
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
width 90%;
|
||||
max-width 420px;
|
||||
}
|
||||
|
||||
.el-message {
|
||||
width 90%;
|
||||
min-width: 300px;
|
||||
max-width 600px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
Loading…
Reference in New Issue
Block a user