From eb22de912a54873b3f80ed2225e596d87851ce48 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Mon, 17 Apr 2023 22:48:37 +0800
Subject: [PATCH 01/30] add usage
---
opencat.go | 2 ++
router/router.go | 39 +++++++++++++++++++++++++++++++++++++++
store/db.go | 11 +++++++++++
store/usage.go | 38 ++++++++++++++++++++++++++++++++++++++
4 files changed, 90 insertions(+)
create mode 100644 store/usage.go
diff --git a/opencat.go b/opencat.go
index 8dbba0b..1859eb9 100644
--- a/opencat.go
+++ b/opencat.go
@@ -38,6 +38,8 @@ func main() {
// 获取所有用户信息
group.GET("/users", router.HandleUsers)
+ group.GET("/usages", router.HandleUsage)
+
// 添加Key
group.POST("/keys", router.HandleAddKey)
diff --git a/router/router.go b/router/router.go
index a18ccd9..6510268 100644
--- a/router/router.go
+++ b/router/router.go
@@ -381,3 +381,42 @@ func HandleReverseProxy(c *gin.Context) {
proxy.ServeHTTP(c.Writer, req)
}
+
+type Usage struct {
+ Cost string `json:"cost"`
+ UserID int `json:"userId"`
+ TotalUnit int `json:"totalUnit"`
+}
+
+func HandleUsage(c *gin.Context) {
+ fromStr := c.Query("from")
+ toStr := c.Query("to")
+
+ from, err := time.Parse("2006-01-02", fromStr)
+ if err != nil {
+ c.JSON(400, gin.H{"error": "Invalid from date format"})
+ return
+ }
+
+ to, err := time.Parse("2006-01-02", toStr)
+ if err != nil {
+ c.JSON(400, gin.H{"error": "Invalid to date format"})
+ return
+ }
+
+ err = store.QueryUsage(from, to)
+ if err != nil {
+ c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
+ return
+ }
+
+ // Mock data for testing
+ usage := Usage{
+ Cost: "0.000076",
+ UserID: 1,
+ TotalUnit: 38,
+ }
+
+ c.JSON(200, []Usage{usage})
+
+}
diff --git a/store/db.go b/store/db.go
index 88b1294..65027bb 100644
--- a/store/db.go
+++ b/store/db.go
@@ -11,6 +11,8 @@ import (
var db *gorm.DB
+var usage *gorm.DB
+
func init() {
if _, err := os.Stat("db"); os.IsNotExist(err) {
errDir := os.MkdirAll("db", 0755)
@@ -31,4 +33,13 @@ func init() {
}
LoadKeysCache()
LoadAuthCache()
+
+ usage, err = gorm.Open(sqlite.Open("./db/usage.db"), &gorm.Config{})
+ if err != nil {
+ panic(err)
+ }
+ err = usage.AutoMigrate(&DailyUsage{}, &Usage{})
+ if err != nil {
+ panic(err)
+ }
}
diff --git a/store/usage.go b/store/usage.go
new file mode 100644
index 0000000..23a755e
--- /dev/null
+++ b/store/usage.go
@@ -0,0 +1,38 @@
+package store
+
+import "time"
+
+type DailyUsage struct {
+ ID int `gorm:"column:id"`
+ UserID int `gorm:"column:user_id primarykey"`
+ Date time.Time `gorm:"column:date"`
+ SKU string `gorm:"column:sku"`
+ PromptUnits int `gorm:"column:prompt_units"`
+ CompletionUnits int `gorm:"column:completion_units"`
+ TotalUnit int `gorm:"column:total_unit"`
+ Cost string `gorm:"column:cost"`
+}
+
+func (DailyUsage) TableName() string {
+ return "daily_usages"
+}
+
+type Usage struct {
+ ID int `gorm:"column:id"`
+ PromptHash string `gorm:"column:prompt_hash"`
+ UserID int `gorm:"column:user_id"`
+ Date time.Time `gorm:"column:date"`
+ SKU string `gorm:"column:sku"`
+ PromptUnits int `gorm:"column:prompt_units"`
+ CompletionUnits int `gorm:"column:completion_units"`
+ TotalUnit int `gorm:"column:total_unit"`
+ Cost string `gorm:"column:cost"`
+}
+
+func (Usage) TableName() string {
+ return "usages"
+}
+
+func QueryUsage(from, to time.Time) error {
+ return nil
+}
From 11dbf4376efb270914ab31233f01d0eb76d3b731 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Tue, 18 Apr 2023 22:46:11 +0800
Subject: [PATCH 02/30] up
---
router/router.go | 39 ++++++++++--------------------
store/usage.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 73 insertions(+), 28 deletions(-)
diff --git a/router/router.go b/router/router.go
index 6510268..733133e 100644
--- a/router/router.go
+++ b/router/router.go
@@ -323,6 +323,7 @@ func HandleProy(c *gin.Context) {
}
if resp.StatusCode == 200 {
// todo
+ log.Println(string(bodyRes))
}
resbody := io.NopCloser(bytes.NewReader(bodyRes))
// 返回 API 响应主体
@@ -382,41 +383,27 @@ func HandleReverseProxy(c *gin.Context) {
}
-type Usage struct {
- Cost string `json:"cost"`
- UserID int `json:"userId"`
- TotalUnit int `json:"totalUnit"`
-}
-
func HandleUsage(c *gin.Context) {
fromStr := c.Query("from")
toStr := c.Query("to")
- from, err := time.Parse("2006-01-02", fromStr)
- if err != nil {
- c.JSON(400, gin.H{"error": "Invalid from date format"})
- return
- }
+ // from, err := time.Parse("2006-01-02", fromStr)
+ // if err != nil {
+ // c.JSON(400, gin.H{"error": "Invalid from date format"})
+ // return
+ // }
- to, err := time.Parse("2006-01-02", toStr)
- if err != nil {
- c.JSON(400, gin.H{"error": "Invalid to date format"})
- return
- }
+ // to, err := time.Parse("2006-01-02", toStr)
+ // if err != nil {
+ // c.JSON(400, gin.H{"error": "Invalid to date format"})
+ // return
+ // }
- err = store.QueryUsage(from, to)
+ usage, err := store.QueryUsage(fromStr, toStr)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
- // Mock data for testing
- usage := Usage{
- Cost: "0.000076",
- UserID: 1,
- TotalUnit: 38,
- }
-
- c.JSON(200, []Usage{usage})
-
+ c.JSON(200, usage)
}
diff --git a/store/usage.go b/store/usage.go
index 23a755e..89810c9 100644
--- a/store/usage.go
+++ b/store/usage.go
@@ -1,6 +1,8 @@
package store
-import "time"
+import (
+ "time"
+)
type DailyUsage struct {
ID int `gorm:"column:id"`
@@ -33,6 +35,62 @@ func (Usage) TableName() string {
return "usages"
}
-func QueryUsage(from, to time.Time) error {
+type Summary struct {
+ UserId int
+ // SumPromptUnits int
+ // SumCompletionUnits int
+ SumTotalUnit int
+ SumCost float64
+}
+type CalcUsage struct {
+ UserID int `json:"userId,omitempty"`
+ TotalUnit int `json:"totalUnit,omitempty"`
+ Cost string `json:"cost,omitempty"`
+}
+
+func QueryUsage(from, to string) ([]CalcUsage, error) {
+ var results = []CalcUsage{}
+ err := usage.Model(&DailyUsage{}).Select(`user_id,
+ --SUM(prompt_units) AS prompt_units,
+ -- SUM(completion_units) AS completion_units,
+ SUM(total_unit) AS total_unit,
+ SUM(cost) AS cost`).
+ Group("user_id").
+ Where("date >= ? AND date < ?", from, to).
+ Find(&results).Error
+ if err != nil {
+ return nil, err
+ }
+ return results, nil
+}
+
+func SumDaily(userid string) ([]Summary, error) {
+ return nil, nil
+}
+
+func SumDailyV2(uid string) error {
+
+ // err := usage.Model(&DailyUsage{}).
+ // Select("user_id, '2023-04-18' as date, sku, SUM(prompt_units) as sum_prompt_units, SUM(completion_units) as sum_completion_units, SUM(total_unit) as sum_total_unit, SUM(cost) as sum_cost").
+ // Where("date >= ?", "2023-04-18").
+ // Where("user_id = ?", 2).
+ // Create(&DailyUsage{}).Error
+ nowstr := time.Now().Format("2006-01-02")
+ err := usage.Exec(`INSERT INTO daily_usages
+ (user_id, date, sku, prompt_units, completion_units, total_unit, cost)
+ SELECT
+ user_id,
+ ?,
+ sku,
+ SUM(prompt_units) AS sum_prompt_units,
+ SUM(completion_units) AS sum_completion_units,
+ SUM(total_unit) AS sum_total_unit,
+ SUM(cost) AS sum_cost
+ FROM usages
+ WHERE date >= ?
+ AND user_id = ?`, nowstr, nowstr, uid).Error
+ if err != nil {
+ return err
+ }
return nil
}
From 499edbb0fd74a71c40df2e7da4498a195cfa7fac Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Wed, 19 Apr 2023 02:06:09 +0800
Subject: [PATCH 03/30] add Completion
---
router/router.go | 45 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 45 insertions(+)
diff --git a/router/router.go b/router/router.go
index 733133e..fdff51d 100644
--- a/router/router.go
+++ b/router/router.go
@@ -41,6 +41,51 @@ type Key struct {
CreatedAt string `json:"createdAt,omitempty"`
}
+type ChatCompletionMessage struct {
+ Role string `json:"role"`
+ Content string `json:"content"`
+
+ // This property isn't in the official documentation, but it's in
+ // the documentation for the official library for python:
+ // - https://github.com/openai/openai-python/blob/main/chatml.md
+ // - https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
+ Name string `json:"name,omitempty"`
+}
+
+type ChatCompletionRequest struct {
+ Model string `json:"model"`
+ Messages []ChatCompletionMessage `json:"messages"`
+ MaxTokens int `json:"max_tokens,omitempty"`
+ Temperature float32 `json:"temperature,omitempty"`
+ TopP float32 `json:"top_p,omitempty"`
+ N int `json:"n,omitempty"`
+ Stream bool `json:"stream,omitempty"`
+ Stop []string `json:"stop,omitempty"`
+ PresencePenalty float32 `json:"presence_penalty,omitempty"`
+ FrequencyPenalty float32 `json:"frequency_penalty,omitempty"`
+ LogitBias map[string]int `json:"logit_bias,omitempty"`
+ User string `json:"user,omitempty"`
+}
+
+type ChatCompletionChoice struct {
+ Index int `json:"index"`
+ Message ChatCompletionMessage `json:"message"`
+ FinishReason string `json:"finish_reason"`
+}
+
+type ChatCompletionResponse struct {
+ ID string `json:"id"`
+ Object string `json:"object"`
+ Created int64 `json:"created"`
+ Model string `json:"model"`
+ Choices []ChatCompletionChoice `json:"choices"`
+ Usage struct {
+ PromptTokens int `json:"prompt_tokens"`
+ CompletionTokens int `json:"completion_tokens"`
+ TotalTokens int `json:"total_tokens"`
+ } `json:"usage"`
+}
+
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if rootToken == "" {
From 1cd06ea1dfebbc215da3c05d3a9da8bd47b81bc2 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Wed, 19 Apr 2023 22:50:39 +0800
Subject: [PATCH 04/30] up
---
opencat.go | 9 +++--
router/router.go | 87 +++++++++++++++++++++++++++++++++++++-----------
2 files changed, 73 insertions(+), 23 deletions(-)
diff --git a/opencat.go b/opencat.go
index 1859eb9..4cb2d53 100644
--- a/opencat.go
+++ b/opencat.go
@@ -59,9 +59,12 @@ func main() {
// 初始化用户
r.POST("/1/users/init", router.Handleinit)
- r.POST("/v1/chat/completions", router.HandleProy)
- r.GET("/v1/models", router.HandleProy)
- r.GET("/v1/dashboard/billing/subscription", router.HandleProy)
+ r.Any("/v1/*proxypath", router.HandleProy)
+
+ // r.POST("/v1/chat/completions", router.HandleProy)
+ // r.GET("/v1/models", router.HandleProy)
+ // r.GET("/v1/dashboard/billing/subscription", router.HandleProy)
+
r.GET("/", func(c *gin.Context) {
c.Writer.WriteHeader(http.StatusOK)
c.Writer.WriteString(`
Api-Keys:https://platform.openai.com/account/api-keys`)
diff --git a/router/router.go b/router/router.go
index fdff51d..13077b3 100644
--- a/router/router.go
+++ b/router/router.go
@@ -1,7 +1,7 @@
package router
import (
- "bytes"
+ "bufio"
"crypto/tls"
"errors"
"fmt"
@@ -11,6 +11,7 @@ import (
"net/http"
"net/http/httputil"
"opencatd-open/store"
+ "strings"
"time"
"github.com/Sakurasan/to"
@@ -20,8 +21,10 @@ import (
)
var (
- rootToken string
- baseUrl = "https://api.openai.com"
+ rootToken string
+ baseUrl = "https://api.openai.com"
+ GPT3Dot5Turbo = "gpt-3.5-turbo"
+ GPT4 = "gpt-4"
)
type User struct {
@@ -44,12 +47,7 @@ type Key struct {
type ChatCompletionMessage struct {
Role string `json:"role"`
Content string `json:"content"`
-
- // This property isn't in the official documentation, but it's in
- // the documentation for the official library for python:
- // - https://github.com/openai/openai-python/blob/main/chatml.md
- // - https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
- Name string `json:"name,omitempty"`
+ Name string `json:"name,omitempty"`
}
type ChatCompletionRequest struct {
@@ -296,6 +294,7 @@ func GenerateToken() string {
func HandleProy(c *gin.Context) {
var localuser bool
+ var isStream bool
auth := c.Request.Header.Get("Authorization")
if len(auth) > 7 && auth[:7] == "Bearer " {
localuser = store.IsExistAuthCache(auth[7:])
@@ -316,8 +315,18 @@ func HandleProy(c *gin.Context) {
}
client.Transport = tr
+ if c.Request.URL.Path == "/v1/chat/completions" {
+ var chatreq = ChatCompletionRequest{}
+ if err := c.BindJSON(&chatreq); err != nil {
+ return
+ // c.AbortWithError(http.StatusBadRequest,)
+ }
+ isStream = chatreq.Stream
+
+ }
+
// 创建 API 请求
- req, err := http.NewRequest(c.Request.Method, baseUrl+c.Request.URL.Path, c.Request.Body)
+ req, err := http.NewRequest(c.Request.Method, baseUrl+c.Request.RequestURI, c.Request.Body)
if err != nil {
log.Println(err)
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
@@ -360,17 +369,18 @@ func HandleProy(c *gin.Context) {
resp.Header.Del("content-security-policy-report-only")
resp.Header.Del("clear-site-data")
- bodyRes, err := io.ReadAll(resp.Body)
- if err != nil {
- log.Println(err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
- return
+ // bodyRes, err := io.ReadAll(resp.Body)
+ // if err != nil {
+ // log.Println(err)
+ // c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ // return
+ // }
+ reader := bufio.NewReader(resp.Body)
+
+ if resp.StatusCode == 200 && isStream {
+ //todo
}
- if resp.StatusCode == 200 {
- // todo
- log.Println(string(bodyRes))
- }
- resbody := io.NopCloser(bytes.NewReader(bodyRes))
+ resbody := io.NopCloser(reader)
// 返回 API 响应主体
c.Writer.WriteHeader(resp.StatusCode)
if _, err := io.Copy(c.Writer, resbody); err != nil {
@@ -452,3 +462,40 @@ func HandleUsage(c *gin.Context) {
c.JSON(200, usage)
}
+
+// todo
+func streamEvent(reader *bufio.Reader) error {
+
+ lineChan := make(chan string, 1)
+
+ timeout := time.AfterFunc(60*time.Second, func() {
+ lineChan <- ""
+ })
+
+ go func() {
+ line, err := reader.ReadString('\n')
+ if err == nil {
+ lineChan <- line
+ }
+ }()
+
+ line := <-lineChan
+
+ timeout.Stop()
+ if line == "" {
+
+ }
+
+ if strings.HasPrefix(line, "data:") {
+ line = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
+ //log.Println("Received data:", line)
+
+ if line == "[DONE]" {
+
+ }
+
+ line = strings.TrimSpace(line)
+
+ }
+ return nil
+}
From 365bc374873ccf5db2ac73d83bfbdb64300f001e Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Sat, 22 Apr 2023 22:40:47 +0800
Subject: [PATCH 05/30] up
---
router/router.go | 93 +++++++++++++++++++++++++++++++-----------------
1 file changed, 61 insertions(+), 32 deletions(-)
diff --git a/router/router.go b/router/router.go
index 13077b3..9484583 100644
--- a/router/router.go
+++ b/router/router.go
@@ -2,7 +2,9 @@ package router
import (
"bufio"
+ "bytes"
"crypto/tls"
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -18,6 +20,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
+ // "github.com/pkoukk/tiktoken-go"
)
var (
@@ -292,6 +295,12 @@ func GenerateToken() string {
return token.String()
}
+type _ struct {
+ model string
+ promptCount int
+ completionCount int
+}
+
func HandleProy(c *gin.Context) {
var localuser bool
var isStream bool
@@ -376,6 +385,7 @@ func HandleProy(c *gin.Context) {
// return
// }
reader := bufio.NewReader(resp.Body)
+ // var resbuf = bytes.NewBuffer(nil)
if resp.StatusCode == 200 && isStream {
//todo
@@ -437,6 +447,18 @@ func HandleReverseProxy(c *gin.Context) {
proxy.ServeHTTP(c.Writer, req)
}
+func Cost(model string, promptCount, completionCount int) float64 {
+ var cost float64
+ switch model {
+ case "gpt-3.5":
+ cost = 0.002 * float64((promptCount+completionCount)/1000)
+ case "gpt-4-32k":
+ cost = 0.06*float64(promptCount/1000) + 0.12*float64(completionCount/1000)
+ case "gpt-4":
+ cost = 0.03*float64(promptCount/1000) + 0.06*float64(completionCount/1000)
+ }
+ return cost
+}
func HandleUsage(c *gin.Context) {
fromStr := c.Query("from")
@@ -463,39 +485,46 @@ func HandleUsage(c *gin.Context) {
c.JSON(200, usage)
}
-// todo
-func streamEvent(reader *bufio.Reader) error {
-
- lineChan := make(chan string, 1)
-
- timeout := time.AfterFunc(60*time.Second, func() {
- lineChan <- ""
- })
-
+func fetchResponseContent(buf *bytes.Buffer, responseBody *bufio.Reader) <-chan string {
+ contentCh := make(chan string)
go func() {
- line, err := reader.ReadString('\n')
- if err == nil {
- lineChan <- line
+ defer close(contentCh)
+ for {
+ line, err := responseBody.ReadString('\n')
+ if err == nil {
+ buf.WriteString(line)
+ if line == "\n" {
+ continue
+ }
+ if strings.HasPrefix(line, "data:") {
+ line = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
+ if strings.HasSuffix(line, "[DONE]") {
+ break
+ }
+ line = strings.TrimSpace(line)
+ }
+
+ dec := json.NewDecoder(strings.NewReader(line))
+ var data map[string]interface{}
+ if err := dec.Decode(&data); err == io.EOF {
+ log.Println("EOF:", err)
+ break
+ } else if err != nil {
+ fmt.Println("Error decoding response:", err)
+ return
+ }
+ if choices, ok := data["choices"].([]interface{}); ok {
+ for _, choice := range choices {
+ choiceMap := choice.(map[string]interface{})
+ if content, ok := choiceMap["delta"].(map[string]interface{})["content"]; ok {
+ contentCh <- content.(string)
+ }
+ }
+ }
+ } else {
+ break
+ }
}
}()
-
- line := <-lineChan
-
- timeout.Stop()
- if line == "" {
-
- }
-
- if strings.HasPrefix(line, "data:") {
- line = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
- //log.Println("Received data:", line)
-
- if line == "[DONE]" {
-
- }
-
- line = strings.TrimSpace(line)
-
- }
- return nil
+ return contentCh
}
From 5ce546672319c0aa9f2475ab2523ac25ad89126a Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Mon, 24 Apr 2023 22:44:03 +0800
Subject: [PATCH 06/30] usage
---
go.mod | 4 ++
go.sum | 10 +++-
router/router.go | 120 +++++++++++++++++++++++++++++++++++++----------
store/usage.go | 75 +++++++++++++++++++++++++----
store/userdb.go | 9 ++++
5 files changed, 183 insertions(+), 35 deletions(-)
diff --git a/go.mod b/go.mod
index a8e9a12..8e2dc49 100644
--- a/go.mod
+++ b/go.mod
@@ -4,16 +4,20 @@ go 1.19
require (
github.com/Sakurasan/to v0.0.0-20180919163141-e72657dd7c7d
+ github.com/duke-git/lancet/v2 v2.1.19
github.com/gin-gonic/gin v1.9.0
github.com/glebarez/sqlite v1.7.0
github.com/google/uuid v1.3.0
github.com/patrickmn/go-cache v2.1.0+incompatible
+ github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
+ github.com/sashabaranov/go-openai v1.9.0
gorm.io/gorm v1.24.6
)
require (
github.com/bytedance/sonic v1.8.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+ github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.20.3 // indirect
diff --git a/go.sum b/go.sum
index b4405a5..3aa27ab 100644
--- a/go.sum
+++ b/go.sum
@@ -9,6 +9,10 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
+github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/duke-git/lancet/v2 v2.1.19 h1:dbRB1m6wOMV1I0ax/3S6ngop8SYM6I7sr+7D9IXjS2E=
+github.com/duke-git/lancet/v2 v2.1.19/go.mod h1:hNcc06mV7qr+crH/0nP+rlC3TB0Q9g5OrVnO8/TGD4c=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -57,12 +61,16 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
+github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w=
+github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M=
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/sashabaranov/go-openai v1.9.0 h1:NoiO++IISxxJ1pRc0n7uZvMGMake0G+FJ1XPwXtprsA=
+github.com/sashabaranov/go-openai v1.9.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -71,8 +79,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
diff --git a/router/router.go b/router/router.go
index 9484583..28f1dbf 100644
--- a/router/router.go
+++ b/router/router.go
@@ -17,10 +17,12 @@ import (
"time"
"github.com/Sakurasan/to"
+ "github.com/duke-git/lancet/v2/cryptor"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
+ "github.com/pkoukk/tiktoken-go"
+ "github.com/sashabaranov/go-openai"
"gorm.io/gorm"
- // "github.com/pkoukk/tiktoken-go"
)
var (
@@ -295,15 +297,24 @@ func GenerateToken() string {
return token.String()
}
-type _ struct {
- model string
- promptCount int
- completionCount int
-}
+// type Tokens struct {
+// UserID int
+// PromptCount int
+// CompletionCount int
+// TotalTokens int
+// Model string
+// PromptHash string
+// }
func HandleProy(c *gin.Context) {
- var localuser bool
- var isStream bool
+ var (
+ localuser bool
+ isStream bool
+ chatreq = openai.ChatCompletionRequest{}
+ chatres = openai.ChatCompletionResponse{}
+ chatlog store.Tokens
+ pre_prompt string
+ )
auth := c.Request.Header.Get("Authorization")
if len(auth) > 7 && auth[:7] == "Bearer " {
localuser = store.IsExistAuthCache(auth[7:])
@@ -324,14 +335,20 @@ func HandleProy(c *gin.Context) {
}
client.Transport = tr
- if c.Request.URL.Path == "/v1/chat/completions" {
- var chatreq = ChatCompletionRequest{}
+ if c.Request.URL.Path == "/v1/chat/completions" && localuser {
+
if err := c.BindJSON(&chatreq); err != nil {
return
- // c.AbortWithError(http.StatusBadRequest,)
+ // c.AbortWithError(http.StatusBadRequest, err)
}
+ chatlog.Model = chatreq.Model
+ for _, m := range chatreq.Messages {
+ pre_prompt += m.Content + "\n"
+ }
+ chatlog.PromptHash = cryptor.Md5String(pre_prompt)
+ chatlog.PromptCount = NumTokensFromMessages(chatreq.Messages, chatreq.Model)
isStream = chatreq.Stream
-
+ chatlog.UserID, _ = store.GetUserID(auth[7:])
}
// 创建 API 请求
@@ -378,17 +395,24 @@ func HandleProy(c *gin.Context) {
resp.Header.Del("content-security-policy-report-only")
resp.Header.Del("clear-site-data")
- // bodyRes, err := io.ReadAll(resp.Body)
- // if err != nil {
- // log.Println(err)
- // c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
- // return
- // }
reader := bufio.NewReader(resp.Body)
- // var resbuf = bytes.NewBuffer(nil)
+ var resbuf = bytes.NewBuffer(nil)
+
+ if resp.StatusCode == 200 && localuser {
+ if isStream {
+ chatlog.CompletionCount = NumTokensFromStr(<-fetchResponseContent(resbuf, reader), chatreq.Model)
+ chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount
+ } else {
+ reader.WriteTo(resbuf)
+ json.NewDecoder(resbuf).Decode(&chatres)
+ chatlog.PromptCount = chatres.Usage.PromptTokens
+ chatlog.CompletionCount = chatres.Usage.CompletionTokens
+ chatlog.TotalTokens = chatres.Usage.TotalTokens
+ }
+ chatlog.Cost = Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount)
+ store.Record(&chatlog)
+ // todo insert usage && calc daily_usage
- if resp.StatusCode == 200 && isStream {
- //todo
}
resbody := io.NopCloser(reader)
// 返回 API 响应主体
@@ -450,12 +474,12 @@ func HandleReverseProxy(c *gin.Context) {
func Cost(model string, promptCount, completionCount int) float64 {
var cost float64
switch model {
- case "gpt-3.5":
+ case "gpt-3.5-turbo", "gpt-3.5-turbo-0301":
cost = 0.002 * float64((promptCount+completionCount)/1000)
- case "gpt-4-32k":
- cost = 0.06*float64(promptCount/1000) + 0.12*float64(completionCount/1000)
- case "gpt-4":
+ case "gpt-4", "gpt-4-0314":
cost = 0.03*float64(promptCount/1000) + 0.06*float64(completionCount/1000)
+ case "gpt-4-32k", "gpt-4-32k-0314":
+ cost = 0.06*float64(promptCount/1000) + 0.12*float64(completionCount/1000)
}
return cost
}
@@ -528,3 +552,49 @@ func fetchResponseContent(buf *bytes.Buffer, responseBody *bufio.Reader) <-chan
}()
return contentCh
}
+
+func NumTokensFromMessages(messages []openai.ChatCompletionMessage, model string) (num_tokens int) {
+ tkm, err := tiktoken.EncodingForModel(model)
+ if err != nil {
+ err = fmt.Errorf("EncodingForModel: %v", err)
+ fmt.Println(err)
+ return
+ }
+
+ var tokens_per_message int
+ var tokens_per_name int
+ if model == "gpt-3.5-turbo-0301" || model == "gpt-3.5-turbo" {
+ tokens_per_message = 4
+ tokens_per_name = -1
+ } else if model == "gpt-4-0314" || model == "gpt-4" {
+ tokens_per_message = 3
+ tokens_per_name = 1
+ } else {
+ fmt.Println("Warning: model not found. Using cl100k_base encoding.")
+ tokens_per_message = 3
+ tokens_per_name = 1
+ }
+
+ for _, message := range messages {
+ num_tokens += tokens_per_message
+ num_tokens += len(tkm.Encode(message.Content, nil, nil))
+ // num_tokens += len(tkm.Encode(message.Role, nil, nil))
+ if message.Name != "" {
+ num_tokens += tokens_per_name
+ }
+ }
+ num_tokens += 3
+ return num_tokens
+}
+
+func NumTokensFromStr(messages string, model string) (num_tokens int) {
+ tkm, err := tiktoken.EncodingForModel(model)
+ if err != nil {
+ err = fmt.Errorf("EncodingForModel: %v", err)
+ fmt.Println(err)
+ return
+ }
+
+ num_tokens += len(tkm.Encode(messages, nil, nil))
+ return num_tokens
+}
diff --git a/store/usage.go b/store/usage.go
index 89810c9..6241011 100644
--- a/store/usage.go
+++ b/store/usage.go
@@ -1,7 +1,10 @@
package store
import (
+ "log"
"time"
+
+ "github.com/Sakurasan/to"
)
type DailyUsage struct {
@@ -23,12 +26,12 @@ type Usage struct {
ID int `gorm:"column:id"`
PromptHash string `gorm:"column:prompt_hash"`
UserID int `gorm:"column:user_id"`
- Date time.Time `gorm:"column:date"`
SKU string `gorm:"column:sku"`
PromptUnits int `gorm:"column:prompt_units"`
CompletionUnits int `gorm:"column:completion_units"`
TotalUnit int `gorm:"column:total_unit"`
Cost string `gorm:"column:cost"`
+ Date time.Time `gorm:"column:date"`
}
func (Usage) TableName() string {
@@ -64,18 +67,49 @@ func QueryUsage(from, to string) ([]CalcUsage, error) {
return results, nil
}
+type Tokens struct {
+ UserID int
+ PromptCount int
+ CompletionCount int
+ TotalTokens int
+ Cost float64
+ Model string
+ PromptHash string
+}
+
+func Record(chatlog *Tokens) (err error) {
+ u := &Usage{
+ UserID: chatlog.UserID,
+ SKU: chatlog.Model,
+ PromptHash: chatlog.PromptHash,
+ PromptUnits: chatlog.PromptCount,
+ CompletionUnits: chatlog.CompletionCount,
+ TotalUnit: chatlog.TotalTokens,
+ Cost: to.String(chatlog.Cost),
+ Date: time.Now(),
+ }
+ err = usage.Create(u).Error
+ return
+
+}
+
func SumDaily(userid string) ([]Summary, error) {
+ var count int64
+ err := usage.Model(&DailyUsage{}).Where("user_id = ? and date = ?", userid, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)).Count(&count).Error
+ if err != nil {
+ log.Println(err)
+ }
+ if count == 0 {
+
+ } else {
+
+ }
+
return nil, nil
}
-func SumDailyV2(uid string) error {
-
- // err := usage.Model(&DailyUsage{}).
- // Select("user_id, '2023-04-18' as date, sku, SUM(prompt_units) as sum_prompt_units, SUM(completion_units) as sum_completion_units, SUM(total_unit) as sum_total_unit, SUM(cost) as sum_cost").
- // Where("date >= ?", "2023-04-18").
- // Where("user_id = ?", 2).
- // Create(&DailyUsage{}).Error
- nowstr := time.Now().Format("2006-01-02")
+func insertSumDaily(uid string) error {
+ nowstr := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
err := usage.Exec(`INSERT INTO daily_usages
(user_id, date, sku, prompt_units, completion_units, total_unit, cost)
SELECT
@@ -94,3 +128,26 @@ func SumDailyV2(uid string) error {
}
return nil
}
+
+func updateSumDaily(uid string, date time.Time) error {
+ var u = Usage{}
+ err := usage.Exec(`SELECT
+ user_id,
+ ?,
+ sku,
+ SUM(prompt_units) AS prompt_units,
+ SUM(completion_units) AS completion_units,
+ SUM(total_unit) AS total_unit,
+ SUM(cost) AS cost
+ FROM usages
+ WHERE date >= ?
+ AND user_id = ?`, date, date, uid).First(&u).Error
+ if err != nil {
+ return err
+ }
+ err = usage.Model(&DailyUsage{}).Where("user_id = ? and date = ?", uid, date).Updates(u).Error
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/store/userdb.go b/store/userdb.go
index e4577c4..1b25c6d 100644
--- a/store/userdb.go
+++ b/store/userdb.go
@@ -73,6 +73,15 @@ func GetUserByName(name string) (*User, error) {
return &user, nil
}
+func GetUserID(authkey string) (int, error) {
+ var user User
+ result := db.Where(&User{Token: authkey}).First(&user)
+ if result.Error != nil {
+ return 0, result.Error
+ }
+ return int(user.ID), nil
+}
+
func GetAllUsers() ([]*User, error) {
var users []*User
result := db.Find(&users)
From 97a13a5f7978cdd870b602d2f7480998f84d1a74 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Tue, 25 Apr 2023 21:30:18 +0800
Subject: [PATCH 07/30] usage
---
router/router.go | 21 ++++++++++++++-----
store/usage.go | 54 +++++++++++++++++++++---------------------------
2 files changed, 40 insertions(+), 35 deletions(-)
diff --git a/router/router.go b/router/router.go
index 28f1dbf..c6b457a 100644
--- a/router/router.go
+++ b/router/router.go
@@ -410,14 +410,25 @@ func HandleProy(c *gin.Context) {
chatlog.TotalTokens = chatres.Usage.TotalTokens
}
chatlog.Cost = Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount)
- store.Record(&chatlog)
- // todo insert usage && calc daily_usage
+ if err := store.Record(&chatlog); err != nil {
+ log.Println(err)
+ }
+ if err := store.SumDaily(chatlog.UserID); err != nil {
+ log.Println(err)
+ }
}
- resbody := io.NopCloser(reader)
- // 返回 API 响应主体
c.Writer.WriteHeader(resp.StatusCode)
- if _, err := io.Copy(c.Writer, resbody); err != nil {
+ if localuser {
+ // 返回 API 响应主体
+ if _, err := io.Copy(c.Writer, resbuf); err != nil {
+ log.Println(err)
+ c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ return
+ }
+ }
+ // 返回 API 响应主体
+ if _, err := io.Copy(c.Writer, io.NopCloser(reader)); err != nil {
log.Println(err)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
diff --git a/store/usage.go b/store/usage.go
index 6241011..26aedf0 100644
--- a/store/usage.go
+++ b/store/usage.go
@@ -1,7 +1,6 @@
package store
import (
- "log"
"time"
"github.com/Sakurasan/to"
@@ -39,11 +38,11 @@ func (Usage) TableName() string {
}
type Summary struct {
- UserId int
- // SumPromptUnits int
- // SumCompletionUnits int
- SumTotalUnit int
- SumCost float64
+ UserId int `gorm:"column:user_id"`
+ SumPromptUnits int `gorm:"column:sum_prompt_units"`
+ SumCompletionUnits int `gorm:"column:sum_completion_units"`
+ SumTotalUnit int `gorm:"column:sum_total_unit"`
+ SumCost float64 `gorm:"column:sum_cost"`
}
type CalcUsage struct {
UserID int `json:"userId,omitempty"`
@@ -93,22 +92,25 @@ func Record(chatlog *Tokens) (err error) {
}
-func SumDaily(userid string) ([]Summary, error) {
+func SumDaily(userid int) error {
var count int64
err := usage.Model(&DailyUsage{}).Where("user_id = ? and date = ?", userid, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)).Count(&count).Error
if err != nil {
- log.Println(err)
+ return err
}
if count == 0 {
-
+ if err := insertSumDaily(userid); err != nil {
+ return err
+ }
} else {
-
+ if err := updateSumDaily(userid, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)); err != nil {
+ return err
+ }
}
-
- return nil, nil
+ return nil
}
-func insertSumDaily(uid string) error {
+func insertSumDaily(uid int) error {
nowstr := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
err := usage.Exec(`INSERT INTO daily_usages
(user_id, date, sku, prompt_units, completion_units, total_unit, cost)
@@ -129,23 +131,15 @@ func insertSumDaily(uid string) error {
return nil
}
-func updateSumDaily(uid string, date time.Time) error {
- var u = Usage{}
- err := usage.Exec(`SELECT
- user_id,
- ?,
- sku,
- SUM(prompt_units) AS prompt_units,
- SUM(completion_units) AS completion_units,
- SUM(total_unit) AS total_unit,
- SUM(cost) AS cost
- FROM usages
- WHERE date >= ?
- AND user_id = ?`, date, date, uid).First(&u).Error
- if err != nil {
- return err
- }
- err = usage.Model(&DailyUsage{}).Where("user_id = ? and date = ?", uid, date).Updates(u).Error
+func updateSumDaily(uid int, date time.Time) error {
+ // var u = Summary{}
+ err := usage.Model(&Usage{}).Exec(`UPDATE daily_usages
+ SET
+ prompt_units = (SELECT SUM(prompt_units) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date),
+ completion_units = (SELECT SUM(completion_units) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date),
+ total_unit = (SELECT SUM(total_unit) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date),
+ cost = (SELECT SUM(cost) FROM usages WHERE user_id = daily_usages.user_id AND date >= daily_usages.date)
+ WHERE user_id = ? AND date >= ?`, uid, date).Error
if err != nil {
return err
}
From 56b56c703d6ba2dc2b89f5b9ed65698e87e00111 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Tue, 25 Apr 2023 22:27:19 +0800
Subject: [PATCH 08/30] up
---
router/router.go | 19 ++++---------------
1 file changed, 4 insertions(+), 15 deletions(-)
diff --git a/router/router.go b/router/router.go
index c6b457a..63f6e3c 100644
--- a/router/router.go
+++ b/router/router.go
@@ -338,8 +338,8 @@ func HandleProy(c *gin.Context) {
if c.Request.URL.Path == "/v1/chat/completions" && localuser {
if err := c.BindJSON(&chatreq); err != nil {
+ c.AbortWithError(http.StatusBadRequest, err)
return
- // c.AbortWithError(http.StatusBadRequest, err)
}
chatlog.Model = chatreq.Model
for _, m := range chatreq.Messages {
@@ -350,9 +350,10 @@ func HandleProy(c *gin.Context) {
isStream = chatreq.Stream
chatlog.UserID, _ = store.GetUserID(auth[7:])
}
-
+ var body bytes.Buffer
+ json.NewEncoder(&body).Encode(chatreq)
// 创建 API 请求
- req, err := http.NewRequest(c.Request.Method, baseUrl+c.Request.RequestURI, c.Request.Body)
+ req, err := http.NewRequest(c.Request.Method, baseUrl+c.Request.RequestURI, &body)
if err != nil {
log.Println(err)
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
@@ -499,18 +500,6 @@ func HandleUsage(c *gin.Context) {
fromStr := c.Query("from")
toStr := c.Query("to")
- // from, err := time.Parse("2006-01-02", fromStr)
- // if err != nil {
- // c.JSON(400, gin.H{"error": "Invalid from date format"})
- // return
- // }
-
- // to, err := time.Parse("2006-01-02", toStr)
- // if err != nil {
- // c.JSON(400, gin.H{"error": "Invalid to date format"})
- // return
- // }
-
usage, err := store.QueryUsage(fromStr, toStr)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
From 3dae04afcdc5f43a3b5e44fe8c4004594e91cec9 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Wed, 26 Apr 2023 03:03:43 +0800
Subject: [PATCH 09/30] fix null data
---
store/usage.go | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/store/usage.go b/store/usage.go
index 26aedf0..91df4e2 100644
--- a/store/usage.go
+++ b/store/usage.go
@@ -1,9 +1,11 @@
package store
import (
+ "errors"
"time"
"github.com/Sakurasan/to"
+ "gorm.io/gorm"
)
type DailyUsage struct {
@@ -95,7 +97,7 @@ func Record(chatlog *Tokens) (err error) {
func SumDaily(userid int) error {
var count int64
err := usage.Model(&DailyUsage{}).Where("user_id = ? and date = ?", userid, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)).Count(&count).Error
- if err != nil {
+ if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if count == 0 {
From a34e579aa8213aa7d2cf38c26c89d5ea6734a9b6 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Wed, 26 Apr 2023 03:18:49 +0800
Subject: [PATCH 10/30] fix db type
---
store/usage.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/store/usage.go b/store/usage.go
index 91df4e2..3a40d31 100644
--- a/store/usage.go
+++ b/store/usage.go
@@ -10,7 +10,7 @@ import (
type DailyUsage struct {
ID int `gorm:"column:id"`
- UserID int `gorm:"column:user_id primarykey"`
+ UserID int `gorm:"column:user_id";primaryKey`
Date time.Time `gorm:"column:date"`
SKU string `gorm:"column:sku"`
PromptUnits int `gorm:"column:prompt_units"`
From a100d75c008ac208ea97982d0687503d64b72489 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Wed, 26 Apr 2023 03:56:29 +0800
Subject: [PATCH 11/30] fix bug
---
router/router.go | 16 ++++++++++------
store/usage.go | 2 +-
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/router/router.go b/router/router.go
index 63f6e3c..34756d8 100644
--- a/router/router.go
+++ b/router/router.go
@@ -401,7 +401,8 @@ func HandleProy(c *gin.Context) {
if resp.StatusCode == 200 && localuser {
if isStream {
- chatlog.CompletionCount = NumTokensFromStr(<-fetchResponseContent(resbuf, reader), chatreq.Model)
+ chatdata := <-fetchResponseContent(resbuf, reader)
+ chatlog.CompletionCount = NumTokensFromStr(chatdata, chatreq.Model)
chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount
} else {
reader.WriteTo(resbuf)
@@ -410,7 +411,7 @@ func HandleProy(c *gin.Context) {
chatlog.CompletionCount = chatres.Usage.CompletionTokens
chatlog.TotalTokens = chatres.Usage.TotalTokens
}
- chatlog.Cost = Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount)
+ chatlog.Cost = fmt.Sprintf("%.6f", Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount))
if err := store.Record(&chatlog); err != nil {
log.Println(err)
}
@@ -484,14 +485,17 @@ func HandleReverseProxy(c *gin.Context) {
}
func Cost(model string, promptCount, completionCount int) float64 {
- var cost float64
+ var cost, prompt, completion float64
+ prompt = float64(promptCount)
+ completion = float64(completionCount)
+
switch model {
case "gpt-3.5-turbo", "gpt-3.5-turbo-0301":
- cost = 0.002 * float64((promptCount+completionCount)/1000)
+ cost = 0.002 * float64((prompt+completion)/1000)
case "gpt-4", "gpt-4-0314":
- cost = 0.03*float64(promptCount/1000) + 0.06*float64(completionCount/1000)
+ cost = 0.03*float64(prompt/1000) + 0.06*float64(completion/1000)
case "gpt-4-32k", "gpt-4-32k-0314":
- cost = 0.06*float64(promptCount/1000) + 0.12*float64(completionCount/1000)
+ cost = 0.06*float64(prompt/1000) + 0.12*float64(completion/1000)
}
return cost
}
diff --git a/store/usage.go b/store/usage.go
index 3a40d31..e2f33d9 100644
--- a/store/usage.go
+++ b/store/usage.go
@@ -73,7 +73,7 @@ type Tokens struct {
PromptCount int
CompletionCount int
TotalTokens int
- Cost float64
+ Cost string
Model string
PromptHash string
}
From a202dfadcacb008bba0cd8064d449230e393dbd7 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Thu, 27 Apr 2023 02:31:35 +0800
Subject: [PATCH 12/30] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
router/router.go | 77 +++++++++++++++++++++++++++---------------------
store/usage.go | 2 +-
2 files changed, 44 insertions(+), 35 deletions(-)
diff --git a/router/router.go b/router/router.go
index 34756d8..c7436ba 100644
--- a/router/router.go
+++ b/router/router.go
@@ -14,6 +14,7 @@ import (
"net/http/httputil"
"opencatd-open/store"
"strings"
+ "sync"
"time"
"github.com/Sakurasan/to"
@@ -297,29 +298,7 @@ func GenerateToken() string {
return token.String()
}
-// type Tokens struct {
-// UserID int
-// PromptCount int
-// CompletionCount int
-// TotalTokens int
-// Model string
-// PromptHash string
-// }
-
-func HandleProy(c *gin.Context) {
- var (
- localuser bool
- isStream bool
- chatreq = openai.ChatCompletionRequest{}
- chatres = openai.ChatCompletionResponse{}
- chatlog store.Tokens
- pre_prompt string
- )
- auth := c.Request.Header.Get("Authorization")
- if len(auth) > 7 && auth[:7] == "Bearer " {
- localuser = store.IsExistAuthCache(auth[7:])
- }
- client := http.DefaultClient
+func getHttpClient() *http.Client {
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
@@ -333,7 +312,23 @@ func HandleProy(c *gin.Context) {
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
- client.Transport = tr
+ return &http.Client{Transport: tr}
+}
+
+func HandleProy(c *gin.Context) {
+ var (
+ localuser bool
+ isStream bool
+ chatreq = openai.ChatCompletionRequest{}
+ chatres = openai.ChatCompletionResponse{}
+ chatlog store.Tokens
+ pre_prompt string
+ wg sync.WaitGroup
+ )
+ auth := c.Request.Header.Get("Authorization")
+ if len(auth) > 7 && auth[:7] == "Bearer " {
+ localuser = store.IsExistAuthCache(auth[7:])
+ }
if c.Request.URL.Path == "/v1/chat/completions" && localuser {
@@ -367,7 +362,7 @@ func HandleProy(c *gin.Context) {
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", store.FromKeyCacheRandomItem()))
}
-
+ client := getHttpClient()
resp, err := client.Do(req)
if err != nil {
log.Println(err)
@@ -396,13 +391,22 @@ func HandleProy(c *gin.Context) {
resp.Header.Del("content-security-policy-report-only")
resp.Header.Del("clear-site-data")
+ c.Writer.WriteHeader(resp.StatusCode)
+ writer := bufio.NewWriter(c.Writer)
+ defer writer.Flush()
+
reader := bufio.NewReader(resp.Body)
var resbuf = bytes.NewBuffer(nil)
if resp.StatusCode == 200 && localuser {
+ wg.Add(1)
if isStream {
- chatdata := <-fetchResponseContent(resbuf, reader)
- chatlog.CompletionCount = NumTokensFromStr(chatdata, chatreq.Model)
+ contentCh := fetchResponseContent(resbuf, reader)
+ var buffer bytes.Buffer
+ for content := range contentCh {
+ buffer.WriteString(content)
+ }
+ chatlog.CompletionCount = NumTokensFromStr(buffer.String(), chatreq.Model)
chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount
} else {
reader.WriteTo(resbuf)
@@ -418,19 +422,24 @@ func HandleProy(c *gin.Context) {
if err := store.SumDaily(chatlog.UserID); err != nil {
log.Println(err)
}
-
- }
- c.Writer.WriteHeader(resp.StatusCode)
- if localuser {
// 返回 API 响应主体
- if _, err := io.Copy(c.Writer, resbuf); err != nil {
- log.Println(err)
+ if _, err := io.Copy(writer, resbuf); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
+ go func() {
+ defer wg.Done()
+ _, err = io.Copy(c.Writer, resp.Body)
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ return
+ }
+ }()
+ wg.Wait()
+ return
}
// 返回 API 响应主体
- if _, err := io.Copy(c.Writer, io.NopCloser(reader)); err != nil {
+ if _, err := io.Copy(writer, reader); err != nil {
log.Println(err)
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
diff --git a/store/usage.go b/store/usage.go
index e2f33d9..d0c3bc3 100644
--- a/store/usage.go
+++ b/store/usage.go
@@ -58,7 +58,7 @@ func QueryUsage(from, to string) ([]CalcUsage, error) {
--SUM(prompt_units) AS prompt_units,
-- SUM(completion_units) AS completion_units,
SUM(total_unit) AS total_unit,
- SUM(cost) AS cost`).
+ printf('%.6f', SUM(cost)) AS cost`).
Group("user_id").
Where("date >= ? AND date < ?", from, to).
Find(&results).Error
From 8a2ebb677879d441835686dc254fdf4d4fe9c557 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Thu, 27 Apr 2023 17:27:43 +0800
Subject: [PATCH 13/30] up
---
router/router.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/router/router.go b/router/router.go
index c7436ba..143053e 100644
--- a/router/router.go
+++ b/router/router.go
@@ -31,6 +31,7 @@ var (
baseUrl = "https://api.openai.com"
GPT3Dot5Turbo = "gpt-3.5-turbo"
GPT4 = "gpt-4"
+ client = getHttpClient()
)
type User struct {
@@ -362,7 +363,7 @@ func HandleProy(c *gin.Context) {
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", store.FromKeyCacheRandomItem()))
}
- client := getHttpClient()
+
resp, err := client.Do(req)
if err != nil {
log.Println(err)
From d2f07a824e9bb990b995900e8dce84c155c11032 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Thu, 27 Apr 2023 22:23:22 +0800
Subject: [PATCH 14/30] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=94=99=E8=AF=AF?=
=?UTF-8?q?=E6=8F=90=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
opencat.go | 3 ++-
router/router.go | 4 +++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/opencat.go b/opencat.go
index 4cb2d53..0ffebbe 100644
--- a/opencat.go
+++ b/opencat.go
@@ -59,7 +59,8 @@ func main() {
// 初始化用户
r.POST("/1/users/init", router.Handleinit)
- r.Any("/v1/*proxypath", router.HandleProy)
+ // r.Any("/v1/*proxypath", router.HandleProy)
+ r.Match([]string{http.MethodGet, http.MethodPost}, "/v1/*proxypath", router.HandleProy)
// r.POST("/v1/chat/completions", router.HandleProy)
// r.GET("/v1/models", router.HandleProy)
diff --git a/router/router.go b/router/router.go
index 143053e..ceb1c08 100644
--- a/router/router.go
+++ b/router/router.go
@@ -358,7 +358,9 @@ func HandleProy(c *gin.Context) {
req.Header = c.Request.Header
if localuser {
if store.KeysCache.ItemCount() == 0 {
- c.JSON(http.StatusOK, gin.H{"error": "No Api-Key Available"})
+ c.JSON(http.StatusBadGateway, gin.H{"error": gin.H{
+ "message": "No Api-Key Available",
+ }})
return
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", store.FromKeyCacheRandomItem()))
From 5200171dc08a57c29d586d30426532458dbe2508 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Fri, 28 Apr 2023 03:05:23 +0800
Subject: [PATCH 15/30] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
router/router.go | 88 +++++++++++++++++++++++++++---------------------
1 file changed, 50 insertions(+), 38 deletions(-)
diff --git a/router/router.go b/router/router.go
index ceb1c08..2fc507b 100644
--- a/router/router.go
+++ b/router/router.go
@@ -14,7 +14,6 @@ import (
"net/http/httputil"
"opencatd-open/store"
"strings"
- "sync"
"time"
"github.com/Sakurasan/to"
@@ -324,7 +323,9 @@ func HandleProy(c *gin.Context) {
chatres = openai.ChatCompletionResponse{}
chatlog store.Tokens
pre_prompt string
- wg sync.WaitGroup
+ req *http.Request
+ err error
+ // wg sync.WaitGroup
)
auth := c.Request.Header.Get("Authorization")
if len(auth) > 7 && auth[:7] == "Bearer " {
@@ -345,16 +346,25 @@ func HandleProy(c *gin.Context) {
chatlog.PromptCount = NumTokensFromMessages(chatreq.Messages, chatreq.Model)
isStream = chatreq.Stream
chatlog.UserID, _ = store.GetUserID(auth[7:])
+
+ var body bytes.Buffer
+ json.NewEncoder(&body).Encode(chatreq)
+ // 创建 API 请求
+ req, err = http.NewRequest(c.Request.Method, baseUrl+c.Request.RequestURI, &body)
+ if err != nil {
+ log.Println(err)
+ c.JSON(http.StatusOK, gin.H{"error": err.Error()})
+ return
+ }
+ } else {
+ req, err = http.NewRequest(c.Request.Method, baseUrl+c.Request.RequestURI, c.Request.Body)
+ if err != nil {
+ log.Println(err)
+ c.JSON(http.StatusOK, gin.H{"error": err.Error()})
+ return
+ }
}
- var body bytes.Buffer
- json.NewEncoder(&body).Encode(chatreq)
- // 创建 API 请求
- req, err := http.NewRequest(c.Request.Method, baseUrl+c.Request.RequestURI, &body)
- if err != nil {
- log.Println(err)
- c.JSON(http.StatusOK, gin.H{"error": err.Error()})
- return
- }
+
req.Header = c.Request.Header
if localuser {
if store.KeysCache.ItemCount() == 0 {
@@ -399,25 +409,38 @@ func HandleProy(c *gin.Context) {
defer writer.Flush()
reader := bufio.NewReader(resp.Body)
- var resbuf = bytes.NewBuffer(nil)
if resp.StatusCode == 200 && localuser {
- wg.Add(1)
+
if isStream {
- contentCh := fetchResponseContent(resbuf, reader)
+ contentCh := fetchResponseContent(writer, reader)
var buffer bytes.Buffer
for content := range contentCh {
buffer.WriteString(content)
}
chatlog.CompletionCount = NumTokensFromStr(buffer.String(), chatreq.Model)
chatlog.TotalTokens = chatlog.PromptCount + chatlog.CompletionCount
- } else {
- reader.WriteTo(resbuf)
- json.NewDecoder(resbuf).Decode(&chatres)
- chatlog.PromptCount = chatres.Usage.PromptTokens
- chatlog.CompletionCount = chatres.Usage.CompletionTokens
- chatlog.TotalTokens = chatres.Usage.TotalTokens
+ chatlog.Cost = fmt.Sprintf("%.6f", Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount))
+ if err := store.Record(&chatlog); err != nil {
+ log.Println(err)
+ }
+ if err := store.SumDaily(chatlog.UserID); err != nil {
+ log.Println(err)
+ }
+ return
}
+ res, err := io.ReadAll(reader)
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{
+ "message": err.Error(),
+ }})
+ return
+ }
+ reader = bufio.NewReader(bytes.NewBuffer(res))
+ json.NewDecoder(bytes.NewBuffer(res)).Decode(&chatres)
+ chatlog.PromptCount = chatres.Usage.PromptTokens
+ chatlog.CompletionCount = chatres.Usage.CompletionTokens
+ chatlog.TotalTokens = chatres.Usage.TotalTokens
chatlog.Cost = fmt.Sprintf("%.6f", Cost(chatlog.Model, chatlog.PromptCount, chatlog.CompletionCount))
if err := store.Record(&chatlog); err != nil {
log.Println(err)
@@ -425,26 +448,14 @@ func HandleProy(c *gin.Context) {
if err := store.SumDaily(chatlog.UserID); err != nil {
log.Println(err)
}
- // 返回 API 响应主体
- if _, err := io.Copy(writer, resbuf); err != nil {
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
- return
- }
- go func() {
- defer wg.Done()
- _, err = io.Copy(c.Writer, resp.Body)
- if err != nil {
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
- return
- }
- }()
- wg.Wait()
- return
+
}
// 返回 API 响应主体
if _, err := io.Copy(writer, reader); err != nil {
log.Println(err)
- c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
+ c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{
+ "message": err.Error(),
+ }})
return
}
}
@@ -525,14 +536,15 @@ func HandleUsage(c *gin.Context) {
c.JSON(200, usage)
}
-func fetchResponseContent(buf *bytes.Buffer, responseBody *bufio.Reader) <-chan string {
+func fetchResponseContent(w *bufio.Writer, responseBody *bufio.Reader) <-chan string {
contentCh := make(chan string)
go func() {
defer close(contentCh)
for {
line, err := responseBody.ReadString('\n')
if err == nil {
- buf.WriteString(line)
+ w.WriteString(line)
+ w.Flush()
if line == "\n" {
continue
}
From 379586cd1bc6b676473c6d0058e7dd0a9936e623 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Fri, 28 Apr 2023 14:46:46 +0800
Subject: [PATCH 16/30] up
---
opencat.go | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/opencat.go b/opencat.go
index 0ffebbe..4cb2d53 100644
--- a/opencat.go
+++ b/opencat.go
@@ -59,8 +59,7 @@ func main() {
// 初始化用户
r.POST("/1/users/init", router.Handleinit)
- // r.Any("/v1/*proxypath", router.HandleProy)
- r.Match([]string{http.MethodGet, http.MethodPost}, "/v1/*proxypath", router.HandleProy)
+ r.Any("/v1/*proxypath", router.HandleProy)
// r.POST("/v1/chat/completions", router.HandleProy)
// r.GET("/v1/models", router.HandleProy)
From ac79ceb04e0ce6ec670560cdf134fb066026b005 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Mon, 1 May 2023 22:31:53 +0800
Subject: [PATCH 17/30] update
---
docker/Dockerfile | 5 +++--
makefile | 5 +++--
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 67013c4..6609c64 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,6 +1,6 @@
FROM golang:1.19.7-alpine as builder
LABEL anther="github.com/Sakurasan"
-RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update && apk --no-cache add openssl make cmake upx
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk --no-cache add make cmake upx
WORKDIR /build
COPY . /build
ENV GO111MODULE=on
@@ -12,9 +12,10 @@ FROM alpine:latest AS runner
# 设置alpine 时间为上海时间
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update && apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
- && apk del tzdata && export GIN_MODE=release
+ && export PATH=$PATH:/app
# RUN apk update && apk --no-cache add openssl libgcc libstdc++ binutils
WORKDIR /app
COPY --from=builder /build/bin/opencatd /app/opencatd
+ENV GIN_MODE=release
EXPOSE 80
ENTRYPOINT ["/app/opencatd"]
\ No newline at end of file
diff --git a/makefile b/makefile
index 111700e..565939d 100644
--- a/makefile
+++ b/makefile
@@ -24,11 +24,12 @@ build:
upx -9 bin/opencatd
.PHONY:docker
+# build docker images
docker:
docker run --privileged --rm tonistiigi/binfmt --install all
- docker buildx create --use --name xbuilder
+ docker buildx create --use --name xbuilder --driver docker-container
docker buildx inspect xbuilder --bootstrap
- docker buildx build --platform linux/amd64,linux/arm64 -t mirrors2/opencatd:latest . --push
+ docker buildx build --platform linux/amd64,linux/arm64 -t mirrors2/opencatd:latest -f docker/Dockerfile . --push
.PHONY: clean
# clean
From 0dc1d13720d2acd5b6491a49ecd652b7d33000e8 Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Tue, 2 May 2023 01:57:42 +0800
Subject: [PATCH 18/30] =?UTF-8?q?=E5=A2=9E=E5=8A=A0root=5Ftoken=E6=9F=A5?=
=?UTF-8?q?=E8=AF=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 11 +++++++----
docker/Dockerfile | 2 +-
opencat.go | 39 ++++++++++++++++++++++++++++++++-------
3 files changed, 40 insertions(+), 12 deletions(-)
diff --git a/README.md b/README.md
index 3d6143a..119e075 100644
--- a/README.md
+++ b/README.md
@@ -30,10 +30,13 @@ or
```
wget https://github.com/mirrors2/opencatd-open/raw/main/docker/docker-compose.yml
```
-## reset root token
-```
-docker exec -it opencatd-open ./opencatd reset_root
-```
+## 支持的命令
+>获取 root 的 token
+ - `docker exec opencatd-open opencatd root_token`
+
+>重置 root 的 token
+ - `docker exec opencatd-open opencatd reset_root`
+
## Q&A
关于证书?
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 6609c64..754d71c 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -12,10 +12,10 @@ FROM alpine:latest AS runner
# 设置alpine 时间为上海时间
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update && apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
- && export PATH=$PATH:/app
# RUN apk update && apk --no-cache add openssl libgcc libstdc++ binutils
WORKDIR /app
COPY --from=builder /build/bin/opencatd /app/opencatd
ENV GIN_MODE=release
+ENV PATH=$PATH:/app
EXPOSE 80
ENTRYPOINT ["/app/opencatd"]
\ No newline at end of file
diff --git a/opencat.go b/opencat.go
index 4cb2d53..749b600 100644
--- a/opencat.go
+++ b/opencat.go
@@ -9,19 +9,44 @@ import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
+ "gorm.io/gorm"
)
func main() {
args := os.Args[1:]
- if len(args) > 0 && args[0] == "reset_root" {
- log.Println("reset root token...")
- ntoken := uuid.NewString()
- if err := store.UpdateUser(uint(1), ntoken); err != nil {
- log.Fatalln(err)
+ if len(args) > 0 {
+ switch args[0] {
+ case "reset_root":
+ log.Println("reset root token...")
+ if _, err := store.GetUserByID(uint(1)); err != nil {
+ if err == gorm.ErrRecordNotFound {
+ log.Println("请在opencat(或其他APP)客户端完成team初始化")
+ return
+ } else {
+ log.Fatalln(err)
+ return
+ }
+ }
+ ntoken := uuid.NewString()
+ if err := store.UpdateUser(uint(1), ntoken); err != nil {
+ log.Fatalln(err)
+ return
+ }
+ log.Println("new root token:", ntoken)
+ return
+ case "root_token":
+ log.Println("reset root token...")
+ if user, err := store.GetUserByID(uint(1)); err != nil {
+ log.Fatalln(err)
+ return
+ } else {
+ log.Println("root token:", user.Token)
+ return
+ }
+ default:
return
}
- log.Println("new root token:", ntoken)
- return
+
}
port := os.Getenv("PORT")
r := gin.Default()
From b38a2151257d1060ee39a34e9c170d03a782f19a Mon Sep 17 00:00:00 2001
From: Sakurasan <1173092237@qq.com>
Date: Sun, 7 May 2023 22:42:18 +0800
Subject: [PATCH 19/30] up
---
assets/logo.svg | 1 +
store/userdb.go | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 assets/logo.svg
diff --git a/assets/logo.svg b/assets/logo.svg
new file mode 100644
index 0000000..a45bb6b
--- /dev/null
+++ b/assets/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/store/userdb.go b/store/userdb.go
index 1b25c6d..b621e7e 100644
--- a/store/userdb.go
+++ b/store/userdb.go
@@ -6,7 +6,7 @@ import (
type User struct {
IsDelete bool `gorm:"default:false" json:"IsDelete"`
- ID uint `gorm:"primarykey" json:"id,omitempty"`
+ ID uint `gorm:"primaryKey;autoIncrement" json:"id,omitempty"`
Name string `gorm:"unique;not null" json:"name,omitempty"`
Token string `gorm:"unique;not null" json:"token,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
From cc6678c201099b78827bd419dadf9a903ea5fcf3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=E8=8F=8C?=
Date: Sat, 13 May 2023 04:41:35 +0800
Subject: [PATCH 20/30] add frontend
---
.gitignore | 1 +
docker/Dockerfile | 6 +
go.mod | 1 +
go.sum | 20 +
makefile | 7 +-
opencat.go | 29 +-
web/.gitignore | 24 +
web/.vscode/extensions.json | 3 +
web/README.md | 7 +
web/index.html | 13 +
web/package-lock.json | 1623 +++++++++++++++++++++++++++++
web/package.json | 21 +
web/postcss.config.js | 6 +
web/src/App.vue | 161 +++
web/src/assets/chatgpt_client.jpg | Bin 0 -> 130612 bytes
web/src/assets/logo.svg | 1 +
web/src/assets/team.jpg | Bin 0 -> 245173 bytes
web/src/assets/usersdomain.jpg | Bin 0 -> 205534 bytes
web/src/components/HelloWorld.vue | 13 +
web/src/main.js | 5 +
web/src/style.css | 93 ++
web/tailwind.config.js | 11 +
web/vite.config.js | 7 +
23 files changed, 2047 insertions(+), 5 deletions(-)
create mode 100644 web/.gitignore
create mode 100644 web/.vscode/extensions.json
create mode 100644 web/README.md
create mode 100644 web/index.html
create mode 100644 web/package-lock.json
create mode 100644 web/package.json
create mode 100644 web/postcss.config.js
create mode 100644 web/src/App.vue
create mode 100644 web/src/assets/chatgpt_client.jpg
create mode 100644 web/src/assets/logo.svg
create mode 100644 web/src/assets/team.jpg
create mode 100644 web/src/assets/usersdomain.jpg
create mode 100644 web/src/components/HelloWorld.vue
create mode 100644 web/src/main.js
create mode 100644 web/src/style.css
create mode 100644 web/tailwind.config.js
create mode 100644 web/vite.config.js
diff --git a/.gitignore b/.gitignore
index e0f0e0c..c2e39c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
bin/
test/
+dist/
*.log
*.db
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 754d71c..7cec6ad 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,7 +1,13 @@
+FROM node:18.12.1-alpine3.16 AS frontend
+WORKDIR /frontend-build
+COPY ./web/ .
+RUN npm install && npm run build && rm -rf node_modules
+
FROM golang:1.19.7-alpine as builder
LABEL anther="github.com/Sakurasan"
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk --no-cache add make cmake upx
WORKDIR /build
+COPY --from=frontend /frontend-build/dist /build/dist
COPY . /build
ENV GO111MODULE=on
# ENV GOPROXY=https://goproxy.cn,direct
diff --git a/go.mod b/go.mod
index 8e2dc49..69b6512 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.19
require (
github.com/Sakurasan/to v0.0.0-20180919163141-e72657dd7c7d
github.com/duke-git/lancet/v2 v2.1.19
+ github.com/gin-contrib/static v0.0.1
github.com/gin-gonic/gin v1.9.0
github.com/glebarez/sqlite v1.7.0
github.com/google/uuid v1.3.0
diff --git a/go.sum b/go.sum
index 3aa27ab..890645e 100644
--- a/go.sum
+++ b/go.sum
@@ -17,21 +17,29 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
+github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
+github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI=
github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -43,18 +51,22 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
@@ -75,6 +87,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -83,6 +96,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
@@ -91,11 +106,14 @@ golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
@@ -103,6 +121,8 @@ google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/makefile b/makefile
index 565939d..c14a6d7 100644
--- a/makefile
+++ b/makefile
@@ -15,11 +15,16 @@ LDFlags=" \
-X 'main.BuildTime=$(BuildTime)' \
-X 'main.BuildGoVersion=$(BuildGoVersion)'"
+.PHONY: web
+# web
+web:
+ cd web && npm install && npm run build && mv dist ..
+
.PHONY: build
# build
build:
# mkdir -p bin/ && go build -ldflags $(LDFlags) -o ./bin/ ./...
- rm -rf qq.tgz /bin/qq
+ rm -rf bin
mkdir -p bin/ && go build -ldflags "-s -w" -o ./bin/opencatd .
upx -9 bin/opencatd
diff --git a/opencat.go b/opencat.go
index 749b600..b5901f6 100644
--- a/opencat.go
+++ b/opencat.go
@@ -1,6 +1,8 @@
package main
import (
+ "embed"
+ "io/fs"
"log"
"net/http"
"opencatd-open/router"
@@ -12,6 +14,18 @@ import (
"gorm.io/gorm"
)
+//go:embed dist/*
+var web embed.FS
+
+func getFileSystem(path string) http.FileSystem {
+ fs, err := fs.Sub(web, path)
+ if err != nil {
+ panic(err)
+ }
+
+ return http.FS(fs)
+}
+
func main() {
args := os.Args[1:]
if len(args) > 0 {
@@ -90,11 +104,18 @@ func main() {
// r.GET("/v1/models", router.HandleProy)
// r.GET("/v1/dashboard/billing/subscription", router.HandleProy)
- r.GET("/", func(c *gin.Context) {
- c.Writer.WriteHeader(http.StatusOK)
- c.Writer.WriteString(`Api-Keys:https://platform.openai.com/account/api-keys`)
- })
+ // r.Use(static.Serve("/", static.LocalFile("dist", false)))
+ idxFS, err := fs.Sub(web, "dist")
+ if err != nil {
+ panic(err)
+ }
+ r.GET("/", gin.WrapH(http.FileServer(http.FS(idxFS))))
+ assetsFS, err := fs.Sub(web, "dist/assets")
+ if err != nil {
+ panic(err)
+ }
+ r.GET("/assets/*filepath", gin.WrapH(http.StripPrefix("/assets/", http.FileServer(http.FS(assetsFS)))))
if port == "" {
port = "80"
}
diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json
new file mode 100644
index 0000000..c0a6e5a
--- /dev/null
+++ b/web/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
+}
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..e62e093
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,7 @@
+# Vue 3 + Vite
+
+This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `
+