package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/exec"
"strings"
"time"
"github.com/Arvintian/chatgpt-web/pkg/controllers"
"github.com/Arvintian/chatgpt-web/pkg/middlewares"
"github.com/Arvintian/chatgpt-web/pkg/utils"
"github.com/Arvintian/go-utils/cmdutil"
"github.com/gin-gonic/gin"
"github.com/spf13/cobra"
"k8s.io/klog/v2"
)
type ChatGPTWebServer struct {
Host string `name:"host" env:"SERVER_HOST" usage:"http bind host" default:"0.0.0.0"`
Port int `name:"port" env:"SERVER_PORT" usage:"http bind port" default:"7080"`
BasicAuthUser string `name:"auth-user" env:"BASIC_AUTH_USER" usage:"http basic auth user"`
BasicAuthPassword string `name:"auth-password" env:"BASIC_AUTH_PASSWORD" usage:"http basic auth password"`
FrontendPath string `name:"frontend-path" env:"FRONTEND_PATH" default:"/app/public" usage:"frontend path"`
SocksProxy string `name:"socks-proxy" env:"SOCKS_PROXY" usage:"socks proxy url"`
ChatSessionTTL int `name:"chat-session-ttl" env:"CHAT_SESSION_TTL" default:"30" usage:"chat session ttl minute"`
ChatMinResponseTokens int `name:"chat-min-response-tokens" env:"CHAT_MIN_RESPONSE_TOKENS" default:"600" usage:"chat min response tokens"`
OpenAIKey string `name:"openapi-key" env:"OPENAI_KEY" usage:"openai key"`
OpenAIBaseURL string `name:"openapi-base-url" env:"OPENAI_BASE_URL" default:"https://api.openai.com/v1" usage:"openai base url"`
OpenAIModel string `name:"openai-model" env:"OPENAI_MODEL" default:"gpt-3.5-turbo-0301" usage:"openai params model"`
OpenAIMaxTokens int `name:"openai-max-tokens" env:"OPENAI_MAX_TOKENS" default:"4096" usage:"openai params max-tokens"`
OpenAITemperature int `name:"openai-temperature" env:"OPENAI_TEMPERATURE" default:"80" usage:"openai params temperature"`
OpenAIPresencePenalty int `name:"openai-presence-penalty" env:"OPENAI_PRESENCE_PENALTY" default:"100" usage:"openai params presence-penalty"`
OpenAIFrequencyPenalty int `name:"openai-frequency-penalty" env:"OPENAI_FREQUENCY_PENALTY" default:"0" usage:"openai params frequency-penalty"`
Version bool `name:"version" usage:"show version"`
}
var Version = "0.0.0-dev"
func (r *ChatGPTWebServer) Run(cmd *cobra.Command, args []string) error {
if r.Version {
return r.ShowVersion()
}
gin.SetMode(gin.ReleaseMode)
if err := r.updateAssetsFiles(); err != nil {
return err
}
go r.startTokenizer(cmd.Context())
go r.httpServer(cmd.Context())
<-cmd.Context().Done()
return nil
}
func (r *ChatGPTWebServer) httpServer(ctx context.Context) {
chatService, err := controllers.NewChatService(r.OpenAIKey, r.OpenAIBaseURL, r.SocksProxy, controllers.ChatCompletionParams{
Model: r.OpenAIModel,
MaxTokens: r.OpenAIMaxTokens,
Temperature: float32(r.OpenAITemperature) / 100.0,
PresencePenalty: float32(r.OpenAIPresencePenalty) / 100.0,
FrequencyPenalty: float32(r.OpenAIFrequencyPenalty) / 100.0,
ChatSessionTTL: time.Duration(r.ChatSessionTTL) * time.Minute,
ChatMinResponseTokens: r.ChatMinResponseTokens,
})
if err != nil {
klog.Fatal(err)
}
addr := fmt.Sprintf("%s:%d", r.Host, r.Port)
klog.Infof("ChatGPT Web Server on: %s", addr)
server := &http.Server{
Addr: addr,
}
entry, proxy := gin.New(), gin.New()
entry.Use(gin.Logger())
entry.Use(gin.Recovery())
chat := entry.Group("/api")
if len(r.BasicAuthUser) > 0 {
accounts := gin.Accounts{}
users := strings.Split(r.BasicAuthUser, ",")
passwords := strings.Split(r.BasicAuthPassword, ",")
if len(users) != len(passwords) {
panic("basic auth setting error")
}
for i := 0; i < len(users); i++ {
accounts[users[i]] = passwords[i]
}
chat.POST("/chat-process", gin.BasicAuth(accounts), middlewares.RateLimitMiddleware(1, 2), chatService.ChatProcess)
} else {
chat.POST("/chat-process", middlewares.RateLimitMiddleware(1, 2), chatService.ChatProcess)
}
chat.POST("/config", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"status": "Success",
"data": map[string]string{
"apiModel": "ChatGPTAPI",
"socksProxy": r.SocksProxy,
},
})
})
chat.POST("/session", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"status": "Success",
"message": "",
"data": gin.H{
"auth": false,
},
})
})
upstreamURL, err := url.Parse(strings.TrimSuffix(r.OpenAIBaseURL, "/v1"))
if err != nil {
klog.Fatal(err)
}
upstream := httputil.NewSingleHostReverseProxy(upstreamURL)
if r.SocksProxy != "" {
proxyUrl, err := url.Parse(r.SocksProxy)
if err != nil {
klog.Fatal(err)
}
upstream.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyUrl),
}
}
apis := proxy.Group("/v1")
apis.Any("/*relativePath", func(ctx *gin.Context) {
ctx.Request.Host = upstreamURL.Host
upstream.ServeHTTP(ctx.Writer, ctx.Request)
})
proxy.NoRoute(func(ctx *gin.Context) {
http.FileServer(http.Dir(r.FrontendPath)).ServeHTTP(ctx.Writer, ctx.Request)
})
entry.NoRoute(func(ctx *gin.Context) {
proxy.ServeHTTP(ctx.Writer, ctx.Request)
})
server.Handler = entry
go func(ctx context.Context) {
<-ctx.Done()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("Server shutdown with error %v", err)
}
}(ctx)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server listen and serve error %v", err)
}
}
func (r *ChatGPTWebServer) startTokenizer(ctx context.Context) {
// devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0755)
// if err != nil {
// klog.Error(err)
// os.Exit(1)
// }
args := strings.Split("nuxt --module tokenizer.py --workers 2", " ")
klog.Infof("Start Tokenizer with %v", args)
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
klog.Error(err)
os.Exit(1)
}
}
func (r *ChatGPTWebServer) updateAssetsFiles() error {
pairs := map[string]string{}
old := `{avatar:"https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg",name:"ChenZhaoYu",description:'Star on Github'}`
new := `{avatar:"https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg",name:"ChatGPT",description:'Star on Github'}`
pairs[old] = new
old = `{}.VITE_GLOB_OPEN_LONG_REPLY`
new = `{VITE_GLOB_OPEN_LONG_REPLY:"true"}.VITE_GLOB_OPEN_LONG_REPLY`
pairs[old] = new
old = ``
new = ``
pairs[old] = new
return utils.ReplaceFiles(r.FrontendPath, pairs)
}
func (r *ChatGPTWebServer) ShowVersion() error {
fmt.Println(Version)
return nil
}
func main() {
root := cmdutil.Command(&ChatGPTWebServer{}, cobra.Command{
Long: "ChatGPT Web Server",
})
cmdutil.Main(root)
}