feat(chat): support other local llm #331

This commit is contained in:
Jacky 2024-05-01 19:40:38 +08:00
parent 08631437ee
commit 3b116b3654
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
4 changed files with 356 additions and 361 deletions

View file

@ -1,305 +1,305 @@
package nginx
import (
"encoding/json"
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/hpcloud/tail"
"github.com/pkg/errors"
"github.com/spf13/cast"
"io"
"net/http"
"os"
"strings"
"encoding/json"
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/hpcloud/tail"
"github.com/pkg/errors"
"github.com/spf13/cast"
"io"
"net/http"
"os"
"strings"
)
const (
PageSize = 128 * 1024
PageSize = 128 * 1024
)
type controlStruct struct {
Type string `json:"type"`
ConfName string `json:"conf_name"`
ServerIdx int `json:"server_idx"`
DirectiveIdx int `json:"directive_idx"`
Type string `json:"type"`
ConfName string `json:"conf_name"`
ServerIdx int `json:"server_idx"`
DirectiveIdx int `json:"directive_idx"`
}
type nginxLogPageResp struct {
Content string `json:"content"`
Page int64 `json:"page"`
Content string `json:"content"`
Page int64 `json:"page"`
}
func GetNginxLogPage(c *gin.Context) {
page := cast.ToInt64(c.Query("page"))
if page < 0 {
page = 0
}
page := cast.ToInt64(c.Query("page"))
if page < 0 {
page = 0
}
var control controlStruct
if !api.BindAndValid(c, &control) {
return
}
var control controlStruct
if !api.BindAndValid(c, &control) {
return
}
logPath, err := getLogPath(&control)
logPath, err := getLogPath(&control)
if err != nil {
logger.Error(err)
return
}
if err != nil {
logger.Error(err)
return
}
logFileStat, err := os.Stat(logPath)
logFileStat, err := os.Stat(logPath)
if err != nil {
c.JSON(http.StatusOK, nginxLogPageResp{})
logger.Error(err)
return
}
if err != nil {
c.JSON(http.StatusOK, nginxLogPageResp{})
logger.Error(err)
return
}
if !logFileStat.Mode().IsRegular() {
c.JSON(http.StatusOK, nginxLogPageResp{})
logger.Error("log file is not regular file:", logPath)
return
}
if !logFileStat.Mode().IsRegular() {
c.JSON(http.StatusOK, nginxLogPageResp{})
logger.Error("log file is not regular file:", logPath)
return
}
f, err := os.Open(logPath)
f, err := os.Open(logPath)
if err != nil {
c.JSON(http.StatusOK, nginxLogPageResp{})
logger.Error(err)
return
}
if err != nil {
c.JSON(http.StatusOK, nginxLogPageResp{})
logger.Error(err)
return
}
totalPage := logFileStat.Size() / PageSize
totalPage := logFileStat.Size() / PageSize
if logFileStat.Size()%PageSize > 0 {
totalPage++
}
if logFileStat.Size()%PageSize > 0 {
totalPage++
}
var buf []byte
var offset int64
if page == 0 {
page = totalPage
}
var buf []byte
var offset int64
if page == 0 {
page = totalPage
}
buf = make([]byte, PageSize)
offset = (page - 1) * PageSize
buf = make([]byte, PageSize)
offset = (page - 1) * PageSize
// seek
_, err = f.Seek(offset, io.SeekStart)
if err != nil && err != io.EOF {
c.JSON(http.StatusOK, nginxLogPageResp{})
logger.Error(err)
return
}
// seek
_, err = f.Seek(offset, io.SeekStart)
if err != nil && err != io.EOF {
c.JSON(http.StatusOK, nginxLogPageResp{})
logger.Error(err)
return
}
n, err := f.Read(buf)
n, err := f.Read(buf)
if err != nil && err != io.EOF {
c.JSON(http.StatusOK, nginxLogPageResp{})
logger.Error(err)
return
}
if err != nil && err != io.EOF {
c.JSON(http.StatusOK, nginxLogPageResp{})
logger.Error(err)
return
}
c.JSON(http.StatusOK, nginxLogPageResp{
Page: page,
Content: string(buf[:n]),
})
c.JSON(http.StatusOK, nginxLogPageResp{
Page: page,
Content: string(buf[:n]),
})
}
func getLogPath(control *controlStruct) (logPath string, err error) {
switch control.Type {
case "site":
var config *nginx.NgxConfig
path := nginx.GetConfPath("sites-available", control.ConfName)
config, err = nginx.ParseNgxConfig(path)
if err != nil {
err = errors.Wrap(err, "error parsing ngx config")
return
}
switch control.Type {
case "site":
var config *nginx.NgxConfig
path := nginx.GetConfPath("sites-available", control.ConfName)
config, err = nginx.ParseNgxConfig(path)
if err != nil {
err = errors.Wrap(err, "error parsing ngx config")
return
}
if control.ServerIdx >= len(config.Servers) {
err = errors.New("serverIdx out of range")
return
}
if control.ServerIdx >= len(config.Servers) {
err = errors.New("serverIdx out of range")
return
}
if control.DirectiveIdx >= len(config.Servers[control.ServerIdx].Directives) {
err = errors.New("DirectiveIdx out of range")
return
}
if control.DirectiveIdx >= len(config.Servers[control.ServerIdx].Directives) {
err = errors.New("DirectiveIdx out of range")
return
}
directive := config.Servers[control.ServerIdx].Directives[control.DirectiveIdx]
switch directive.Directive {
case "access_log", "error_log":
// ok
default:
err = errors.New("directive.Params neither access_log nor error_log")
return
}
directive := config.Servers[control.ServerIdx].Directives[control.DirectiveIdx]
switch directive.Directive {
case "access_log", "error_log":
// ok
default:
err = errors.New("directive.Params neither access_log nor error_log")
return
}
if directive.Params == "" {
err = errors.New("directive.Params is empty")
return
}
if directive.Params == "" {
err = errors.New("directive.Params is empty")
return
}
// fix: access_log /var/log/test.log main;
p := strings.Split(directive.Params, " ")
if len(p) > 0 {
logPath = p[0]
}
// fix: access_log /var/log/test.log main;
p := strings.Split(directive.Params, " ")
if len(p) > 0 {
logPath = p[0]
}
case "error":
path := nginx.GetErrorLogPath()
case "error":
path := nginx.GetErrorLogPath()
if path == "" {
err = errors.New("settings.NginxLogSettings.ErrorLogPath is empty," +
" refer to https://nginxui.com/guide/config-nginx.html for more information")
return
}
if path == "" {
err = errors.New("settings.NginxLogSettings.ErrorLogPath is empty," +
" refer to https://nginxui.com/guide/config-nginx.html for more information")
return
}
logPath = path
default:
path := nginx.GetAccessLogPath()
logPath = path
default:
path := nginx.GetAccessLogPath()
if path == "" {
err = errors.New("settings.NginxLogSettings.AccessLogPath is empty," +
" refer to https://nginxui.com/guide/config-nginx.html for more information")
return
}
if path == "" {
err = errors.New("settings.NginxLogSettings.AccessLogPath is empty," +
" refer to https://nginxui.com/guide/config-nginx.html for more information")
return
}
logPath = path
}
logPath = path
}
return
return
}
func tailNginxLog(ws *websocket.Conn, controlChan chan controlStruct, errChan chan error) {
defer func() {
if err := recover(); err != nil {
logger.Error(err)
return
}
}()
defer func() {
if err := recover(); err != nil {
logger.Error(err)
return
}
}()
control := <-controlChan
control := <-controlChan
for {
logPath, err := getLogPath(&control)
for {
logPath, err := getLogPath(&control)
if err != nil {
errChan <- err
return
}
if err != nil {
errChan <- err
return
}
seek := tail.SeekInfo{
Offset: 0,
Whence: io.SeekEnd,
}
seek := tail.SeekInfo{
Offset: 0,
Whence: io.SeekEnd,
}
stat, err := os.Stat(logPath)
if os.IsNotExist(err) {
errChan <- errors.New("[error] log path not exists " + logPath)
return
}
stat, err := os.Stat(logPath)
if os.IsNotExist(err) {
errChan <- errors.New("[error] log path not exists " + logPath)
return
}
if !stat.Mode().IsRegular() {
errChan <- errors.New("[error] " + logPath + " is not a regular file. " +
"If you are using nginx-ui in docker container, please refer to " +
"https://nginxui.com/zh_CN/guide/config-nginx-log.html for more information.")
return
}
if !stat.Mode().IsRegular() {
errChan <- errors.New("[error] " + logPath + " is not a regular file. " +
"If you are using nginx-ui in docker container, please refer to " +
"https://nginxui.com/zh_CN/guide/config-nginx-log.html for more information.")
return
}
// Create a tail
t, err := tail.TailFile(logPath, tail.Config{Follow: true,
ReOpen: true, Location: &seek})
// Create a tail
t, err := tail.TailFile(logPath, tail.Config{Follow: true,
ReOpen: true, Location: &seek})
if err != nil {
errChan <- errors.Wrap(err, "error tailing log")
return
}
if err != nil {
errChan <- errors.Wrap(err, "error tailing log")
return
}
for {
var next = false
select {
case line := <-t.Lines:
// Print the text of each received line
if line == nil {
continue
}
for {
var next = false
select {
case line := <-t.Lines:
// Print the text of each received line
if line == nil {
continue
}
err = ws.WriteMessage(websocket.TextMessage, []byte(line.Text))
err = ws.WriteMessage(websocket.TextMessage, []byte(line.Text))
if err != nil && websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
errChan <- errors.Wrap(err, "error tailNginxLog write message")
return
}
case control = <-controlChan:
next = true
break
}
if next {
break
}
}
}
if err != nil && websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
errChan <- errors.Wrap(err, "error tailNginxLog write message")
return
}
case control = <-controlChan:
next = true
break
}
if next {
break
}
}
}
}
func handleLogControl(ws *websocket.Conn, controlChan chan controlStruct, errChan chan error) {
defer func() {
if err := recover(); err != nil {
logger.Error(err)
return
}
}()
defer func() {
if err := recover(); err != nil {
logger.Error(err)
return
}
}()
for {
msgType, payload, err := ws.ReadMessage()
if err != nil && websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
errChan <- errors.Wrap(err, "error handleLogControl read message")
return
}
for {
msgType, payload, err := ws.ReadMessage()
if err != nil && websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) {
errChan <- errors.Wrap(err, "error handleLogControl read message")
return
}
if msgType != websocket.TextMessage {
errChan <- errors.New("error handleLogControl message type")
return
}
if msgType != websocket.TextMessage {
errChan <- errors.New("error handleLogControl message type")
return
}
var msg controlStruct
err = json.Unmarshal(payload, &msg)
if err != nil {
errChan <- errors.Wrap(err, "error ReadWsAndWritePty json.Unmarshal")
return
}
controlChan <- msg
}
var msg controlStruct
err = json.Unmarshal(payload, &msg)
if err != nil {
errChan <- errors.Wrap(err, "error ReadWsAndWritePty json.Unmarshal")
return
}
controlChan <- msg
}
}
func Log(c *gin.Context) {
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// upgrade http to websocket
ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(err)
return
}
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// upgrade http to websocket
ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(err)
return
}
defer ws.Close()
defer ws.Close()
errChan := make(chan error, 1)
controlChan := make(chan controlStruct, 1)
errChan := make(chan error, 1)
controlChan := make(chan controlStruct, 1)
go tailNginxLog(ws, controlChan, errChan)
go handleLogControl(ws, controlChan, errChan)
go tailNginxLog(ws, controlChan, errChan)
go handleLogControl(ws, controlChan, errChan)
if err = <-errChan; err != nil {
logger.Error(err)
_ = ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
return
}
if err = <-errChan; err != nil {
logger.Error(err)
_ = ws.WriteMessage(websocket.TextMessage, []byte(err.Error()))
return
}
}

View file

@ -1,134 +1,123 @@
package openai
import (
"context"
"crypto/tls"
"fmt"
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/sashabaranov/go-openai"
"io"
"net/http"
"net/url"
"os"
"context"
"crypto/tls"
"fmt"
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/sashabaranov/go-openai"
"io"
"net/http"
"net/url"
"os"
)
const ChatGPTInitPrompt = "You are a assistant who can help users write and optimise the configurations of Nginx, the first user message contains the content of the configuration file which is currently opened by the user and the current language code(CLC). You suppose to use the language corresponding to the CLC to give the first reply. Later the language environment depends on the user message. The first reply should involve the key information of the file and ask user what can you help them."
func MakeChatCompletionRequest(c *gin.Context) {
var json struct {
Messages []openai.ChatCompletionMessage `json:"messages"`
}
var json struct {
Messages []openai.ChatCompletionMessage `json:"messages"`
}
if !api.BindAndValid(c, &json) {
return
}
if !api.BindAndValid(c, &json) {
return
}
messages := []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: ChatGPTInitPrompt,
},
}
messages = append(messages, json.Messages...)
// sse server
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
messages := []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: ChatGPTInitPrompt,
},
}
messages = append(messages, json.Messages...)
// sse server
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
if settings.OpenAISettings.Token == "" {
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", gin.H{
"type": "error",
"content": "[Error] OpenAI token is empty",
})
return false
})
return
}
config := openai.DefaultConfig(settings.OpenAISettings.Token)
config := openai.DefaultConfig(settings.OpenAISettings.Token)
if settings.OpenAISettings.Proxy != "" {
proxyUrl, err := url.Parse(settings.OpenAISettings.Proxy)
if err != nil {
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", gin.H{
"type": "error",
"content": err.Error(),
})
return false
})
return
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyUrl),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
config.HTTPClient = &http.Client{
Transport: transport,
}
}
if settings.OpenAISettings.Proxy != "" {
proxyUrl, err := url.Parse(settings.OpenAISettings.Proxy)
if err != nil {
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", gin.H{
"type": "error",
"content": err.Error(),
})
return false
})
return
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyUrl),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
config.HTTPClient = &http.Client{
Transport: transport,
}
}
if settings.OpenAISettings.BaseUrl != "" {
config.BaseURL = settings.OpenAISettings.BaseUrl
}
if settings.OpenAISettings.BaseUrl != "" {
config.BaseURL = settings.OpenAISettings.BaseUrl
}
openaiClient := openai.NewClientWithConfig(config)
ctx := context.Background()
openaiClient := openai.NewClientWithConfig(config)
ctx := context.Background()
req := openai.ChatCompletionRequest{
Model: settings.OpenAISettings.Model,
Messages: messages,
Stream: true,
}
stream, err := openaiClient.CreateChatCompletionStream(ctx, req)
if err != nil {
fmt.Printf("CompletionStream error: %v\n", err)
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", gin.H{
"type": "error",
"content": err.Error(),
})
return false
})
return
}
defer stream.Close()
msgChan := make(chan string)
go func() {
defer close(msgChan)
for {
response, err := stream.Recv()
if errors.Is(err, io.EOF) {
fmt.Println()
return
}
req := openai.ChatCompletionRequest{
Model: settings.OpenAISettings.Model,
Messages: messages,
Stream: true,
}
stream, err := openaiClient.CreateChatCompletionStream(ctx, req)
if err != nil {
fmt.Printf("CompletionStream error: %v\n", err)
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", gin.H{
"type": "error",
"content": err.Error(),
})
return false
})
return
}
defer stream.Close()
msgChan := make(chan string)
go func() {
defer close(msgChan)
for {
response, err := stream.Recv()
if errors.Is(err, io.EOF) {
fmt.Println()
return
}
if err != nil {
fmt.Printf("Stream error: %v\n", err)
return
}
if err != nil {
fmt.Printf("Stream error: %v\n", err)
return
}
message := fmt.Sprintf("%s", response.Choices[0].Delta.Content)
fmt.Printf("%s", message)
_ = os.Stdout.Sync()
message := fmt.Sprintf("%s", response.Choices[0].Delta.Content)
fmt.Printf("%s", message)
_ = os.Stdout.Sync()
msgChan <- message
}
}()
msgChan <- message
}
}()
c.Stream(func(w io.Writer) bool {
if m, ok := <-msgChan; ok {
c.SSEvent("message", gin.H{
"type": "message",
"content": m,
})
return true
}
return false
})
c.Stream(func(w io.Writer) bool {
if m, ok := <-msgChan; ok {
c.SSEvent("message", gin.H{
"type": "message",
"content": m,
})
return true
}
return false
})
}