Implemented feature: chatgpt api stream real time reception

This commit is contained in:
Zhang Minghan 2023-07-22 22:57:56 +08:00
parent 18722a6567
commit e6673e1c36
8 changed files with 215 additions and 5 deletions

60
api/chat.go Normal file
View File

@ -0,0 +1,60 @@
package api
import (
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"net/http"
)
func ChatAPI(c *gin.Context) {
// websocket connection
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"message": "",
"reason": err.Error(),
})
return
}
defer func(conn *websocket.Conn) {
err := conn.Close()
if err != nil {
return
}
}(conn)
for {
_, message, err := conn.ReadMessage()
if err != nil {
return
}
var form map[string]interface{}
if err := json.Unmarshal(message, &form); err == nil {
message := form["message"].(string)
StreamRequest("gpt-3.5-turbo-16k", []ChatGPTMessage{
{
Role: "user",
Content: message,
},
}, 250, func(resp string) {
data, _ := json.Marshal(map[string]interface{}{
"message": resp,
"end": false,
})
_ = conn.WriteMessage(websocket.TextMessage, data)
})
data, _ := json.Marshal(map[string]interface{}{
"message": "",
"end": true,
})
_ = conn.WriteMessage(websocket.TextMessage, data)
}
}
}

View File

@ -42,7 +42,7 @@ func StreamRequest(model string, messages []ChatGPTMessage, token int, callback
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
client := &http.Client{}
req, err := http.NewRequest("POST", viper.GetString("openai.user_endpoint")+"/chat/completions", utils.ConvertBody(ChatGPTRequest{
req, err := http.NewRequest("POST", viper.GetString("openai.anonymous_endpoint")+"/chat/completions", utils.ConvertBody(ChatGPTRequest{
Model: model,
Messages: messages,
MaxToken: token,
@ -53,7 +53,7 @@ func StreamRequest(model string, messages []ChatGPTMessage, token int, callback
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+viper.GetString("openai.user"))
req.Header.Set("Authorization", "Bearer "+viper.GetString("openai.anonymous"))
res, err := client.Do(req)
if err != nil {
@ -67,7 +67,7 @@ func StreamRequest(model string, messages []ChatGPTMessage, token int, callback
}
for {
buf := make([]byte, 1024)
buf := make([]byte, 20480)
n, err := res.Body.Read(buf)
if err == io.EOF {

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import Login from "./components/icons/login.vue";
import {auth} from "./assets/script/auth";
import {auth, username} from "./assets/script/auth";
function goto() {
window.location.href = "https://deeptrain.net/login?app=chatnio";
@ -16,7 +16,8 @@ function goto() {
</div>
<div class="grow" />
<div class="user" v-if="auth">
<img class="avatar" src="https://zmh-program.site/avatar/zmh-program.webp" alt="">
<span class="username">{{ username }}</span>
</div>
<div class="login" v-else>
<button @click="goto">
@ -59,6 +60,31 @@ aside {
width: max-content;
}
.user {
display: flex;
flex-direction: row;
margin: 28px auto;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 8px;
background: var(--card-input);
border: 1px solid var(--card-input-border);
transition: .5s;
flex-shrink: 0;
user-select: none;
}
.username {
user-select: none;
font-size: 18px;
padding: 4px;
margin: 0 4px;
color: var(--card-text);
}
.grow {
flex-grow: 1;
}
@ -130,6 +156,16 @@ aside {
}
}
@media screen and (max-width: 340px) {
.username {
display: none;
}
.avatar {
margin-right: 16px;
}
}
@media screen and (max-width: 600px) {
.card {
flex-direction: column;
@ -137,6 +173,10 @@ aside {
max-height: calc(100% - 24px);
}
.username {
margin-right: 10px;
}
.logo span {
display: none;
}

View File

@ -3,6 +3,7 @@ import axios from "axios";
export const auth = ref<boolean | undefined>(undefined);
export const token = ref(localStorage.getItem("token") || "");
export const username = ref("");
watch(token, () => {
localStorage.setItem("token", token.value);
@ -15,6 +16,7 @@ export async function awaitUtilSetup(): Promise<any> {
if (!token.value) return (auth.value = false);
try {
const resp = await axios.post("/state");
username.value = resp.data.user;
auth.value = resp.data.status;
} catch {
auth.value = false;

View File

@ -1,6 +1,8 @@
import {nextTick, reactive, ref} from "vue";
import type { Ref } from "vue";
import axios from "axios";
import {auth} from "./auth";
import {ws_api} from "./conf";
type Message = {
content: string;
@ -9,12 +11,64 @@ type Message = {
stamp: number;
}
type StreamMessage = {
message: string;
end: boolean;
}
export class Connection {
protected connection: WebSocket | undefined;
protected callback?: (message: StreamMessage) => void;
public state: boolean;
public constructor() {
this.state = false;
this.init();
}
public init(): void {
this.connection = new WebSocket(ws_api + "/chat");
this.state = false;
this.connection.onopen = () => {
this.state = true;
}
this.connection.onclose = () => {
this.state = false;
setTimeout(() => {
this.init();
}, 3000);
}
this.connection.onmessage = (event) => {
const message = JSON.parse(event.data);
this.callback && this.callback(message as StreamMessage);
}
}
public send(content: Record<string, any>): boolean {
if (!this.state || !this.connection) {
console.debug("Connection not ready");
return false;
}
this.connection.send(JSON.stringify(content));
return true;
}
public close(): void {
if (!this.connection) return;
this.connection.close();
}
public setCallback(callback: (message: StreamMessage) => void): void {
this.callback = callback;
}
}
export class Conversation {
id: number;
messages: Message[];
len: Ref<number>;
state: Ref<boolean>;
refresh: () => void;
connection: Connection | undefined;
public constructor(id: number, refresh: () => void) {
this.id = id;
@ -22,9 +76,32 @@ export class Conversation {
this.state = ref(false);
this.len = ref(0);
this.refresh = refresh;
if (auth.value) this.connection = new Connection();
}
public async send(content: string): Promise<void> {
return await (auth.value ? this.sendAuthenticated(content) : this.sendAnonymous(content));
}
public async sendAuthenticated(content: string): Promise<void> {
this.state.value = true;
this.addMessageFromUser(content);
let message = ref(""), end = ref(false);
this.connection?.setCallback((res: StreamMessage) => {
message.value += res.message;
end.value = res.end;
})
this.addDynamicMessageFromAI(message, end);
const status = this.connection?.send({
message: content,
});
if (!status) {
this.addMessageFromAI("网络错误,请稍后再试");
return;
}
}
public async sendAnonymous(content: string): Promise<void> {
this.state.value = true;
this.addMessageFromUser(content);
try {
@ -68,6 +145,16 @@ export class Conversation {
this.typingEffect(this.len.value - 1, content);
}
public addDynamicMessageFromAI(content: Ref<string>, end: Ref<boolean>): void {
this.addMessage({
content: "",
role: "bot",
time: new Date().toLocaleTimeString(),
stamp: new Date().getTime(),
})
this.dynamicTypingEffect(this.len.value - 1, content, end);
}
public typingEffect(index: number, content: string): void {
let cursor = 0;
const interval = setInterval(() => {
@ -81,6 +168,22 @@ export class Conversation {
}, 35);
}
public dynamicTypingEffect(index: number, content: Ref<string>, end: Ref<boolean>): void {
let cursor = 0;
const interval = setInterval(() => {
if (end.value && cursor >= content.value.length) {
this.messages[index].content = content.value;
this.state.value = false;
clearInterval(interval);
return;
}
if (cursor >= content.value.length) return;
cursor++;
this.messages[index].content = content.value.substring(0, cursor);
this.refresh();
}, 35);
}
public getMessages(): Message[] {
return this.messages;
}

1
go.mod
View File

@ -22,6 +22,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect

2
go.sum
View File

@ -150,6 +150,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=

View File

@ -23,8 +23,10 @@ func main() {
app.Use(auth.Middleware())
app.POST("/anonymous", api.AnonymousAPI)
app.GET("/chat", api.ChatAPI)
app.POST("/login", auth.LoginAPI)
app.POST("/state", auth.StateAPI)
}
if viper.GetBool("debug") {
gin.SetMode(gin.DebugMode)