From b617af3e27110cc18ce31cc1a4c23e0df6973267 Mon Sep 17 00:00:00 2001 From: zeke Date: Mon, 25 Nov 2024 17:13:51 +0800 Subject: [PATCH] go --- .gitignore | 2 + api.go | 49 ++++++++++++ go.mod | 36 +++++++++ go.sum | 90 ++++++++++++++++++++++ handlers.go | 138 +++++++++++++++++++++++++++++++++ main.go | 70 +++++++++++++++++ package-lock.json | 27 ++++--- package.json | 3 +- process.go | 190 ++++++++++++++++++++++++++++++++++++++++++++++ types.go | 45 +++++++++++ utils.go | 83 ++++++++++++++++++++ yarn.lock | 44 +++++++---- 12 files changed, 749 insertions(+), 28 deletions(-) create mode 100644 api.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handlers.go create mode 100644 main.go create mode 100644 process.go create mode 100644 types.go create mode 100644 utils.go diff --git a/.gitignore b/.gitignore index 896bbfb..553f947 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .env tests/ + +node_modules/ \ No newline at end of file diff --git a/api.go b/api.go new file mode 100644 index 0000000..3509480 --- /dev/null +++ b/api.go @@ -0,0 +1,49 @@ +package main + +import ( + "bytes" + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func formatMessages(messages []Message) string { + var formatted []string + for _, msg := range messages { + formatted = append(formatted, fmt.Sprintf("%s:%s", msg.Role, msg.Content)) + } + return strings.Join(formatted, "\n") +} + +func sendToCursorAPI(c *gin.Context, hexData []byte) (*http.Response, error) { + req, err := http.NewRequest("POST", "https://api2.cursor.sh/aiserver.v1.AiService/StreamChat", bytes.NewReader(hexData)) + if err != nil { + return nil, err + } + + // 获取认证token + authToken := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ") + if strings.Contains(authToken, "%3A%3A") { + authToken = strings.Split(authToken, "%3A%3A")[1] + } + + // 设置请求头 + req.Header.Set("Content-Type", "application/connect+proto") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken)) + req.Header.Set("Connect-Accept-Encoding", "gzip,br") + req.Header.Set("Connect-Protocol-Version", "1") + req.Header.Set("User-Agent", "connect-es/1.4.0") + req.Header.Set("X-Amzn-Trace-Id", fmt.Sprintf("Root=%s", uuid.New().String())) + req.Header.Set("X-Cursor-Checksum", "zo6Qjequ9b9734d1f13c3438ba25ea31ac93d9287248b9d30434934e9fcbfa6b3b22029e/7e4af391f67188693b722eff0090e8e6608bca8fa320ef20a0ccb5d7d62dfdef") + req.Header.Set("X-Cursor-Client-Version", "0.42.3") + req.Header.Set("X-Cursor-Timezone", "Asia/Shanghai") + req.Header.Set("X-Ghost-Mode", "false") + req.Header.Set("X-Request-Id", uuid.New().String()) + req.Header.Set("Host", "api2.cursor.sh") + + client := &http.Client{} + return client.Do(req) +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4c82cdf --- /dev/null +++ b/go.mod @@ -0,0 +1,36 @@ +module cursor-api-proxy + +go 1.21 + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a810f58 --- /dev/null +++ b/go.sum @@ -0,0 +1,90 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +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-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +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.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.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +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= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +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/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +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/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= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..3f01363 --- /dev/null +++ b/handlers.go @@ -0,0 +1,138 @@ +package main + +import ( + "bufio" + "encoding/json" + "io" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func handleStreamResponse(c *gin.Context, req ChatRequest) { + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + + responseId := "chatcmpl-" + uuid.New().String() + + // 准备请求数据 + messages := formatMessages(req.Messages) + hexData := stringToHex(messages, req.Model) + + // 发送请求到 Cursor API + resp, err := sendToCursorAPI(c, hexData) + if err != nil { + c.SSEvent("error", gin.H{"error": "Internal server error"}) + return + } + defer resp.Body.Close() + + reader := bufio.NewReader(resp.Body) + for { + chunk, err := reader.ReadBytes('\n') + if err == io.EOF { + break + } + if err != nil { + continue + } + + text := processChunk(chunk) + if text == "" { + continue + } + + streamResp := StreamResponse{ + ID: responseId, + Object: "chat.completion.chunk", + Created: time.Now().Unix(), + Model: req.Model, + Choices: []struct { + Index int `json:"index"` + Delta struct { + Content string `json:"content"` + } `json:"delta"` + }{{ + Index: 0, + Delta: struct { + Content string `json:"content"` + }{ + Content: text, + }, + }}, + } + + data, _ := json.Marshal(streamResp) + c.SSEvent("message", string(data)) + c.Writer.Flush() + } + + c.SSEvent("message", "[DONE]") +} + +func handleNormalResponse(c *gin.Context, req ChatRequest) { + messages := formatMessages(req.Messages) + hexData := stringToHex(messages, req.Model) + + resp, err := sendToCursorAPI(c, hexData) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"}) + return + } + defer resp.Body.Close() + + var fullText strings.Builder + reader := bufio.NewReader(resp.Body) + for { + chunk, err := reader.ReadBytes('\n') + if err == io.EOF { + break + } + if err != nil { + continue + } + + text := processChunk(chunk) + fullText.WriteString(text) + } + + // 处理响应文本 + text := fullText.String() + text = strings.TrimSpace(strings.TrimPrefix(text, "<|END_USER|>")) + + response := ChatResponse{ + ID: "chatcmpl-" + uuid.New().String(), + Object: "chat.completion", + Created: time.Now().Unix(), + Model: req.Model, + Choices: []struct { + Index int `json:"index"` + Message struct { + Role string `json:"role"` + Content string `json:"content"` + } `json:"message"` + FinishReason string `json:"finish_reason"` + }{{ + Index: 0, + Message: struct { + Role string `json:"role"` + Content string `json:"content"` + }{ + Role: "assistant", + Content: text, + }, + FinishReason: "stop", + }}, + Usage: struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + }{}, + } + + c.JSON(http.StatusOK, response) +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..85caa2f --- /dev/null +++ b/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "log" + "net/http" + "os" + "strings" + + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" +) + +func main() { + if err := godotenv.Load(); err != nil { + log.Println("Warning: Error loading .env file") + } + + r := gin.Default() + r.POST("/v1/chat/completions", handleChat) + + port := os.Getenv("PORT") + if port == "" { + port = "3000" + } + + log.Printf("服务器运行在端口 %s\n", port) + r.Run(":" + port) +} + +func handleChat(c *gin.Context) { + var req ChatRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 检查是否为 o1 开头的模型且请求流式输出 + if strings.HasPrefix(req.Model, "o1-") && req.Stream { + c.JSON(http.StatusBadRequest, gin.H{"error": "Model not supported stream"}) + return + } + + // 获取并处理认证token + authHeader := c.GetHeader("Authorization") + authToken := strings.TrimPrefix(authHeader, "Bearer ") + + if authToken == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Authorization is required", + }) + return + } + + // 处理消息 + if len(req.Messages) == 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Messages should be a non-empty array", + }) + return + } + + // 处理流式请求 + if req.Stream { + handleStreamResponse(c, req) + return + } + + // 处理非流式请求 + handleNormalResponse(c, req) +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 494495e..c4eea19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,9 @@ "axios": "1.7.7", "body-parser": "1.20.3", "dotenv": "16.4.5", - "eventsource-parser": "3.0.0", "express": "4.21.1", - "uuid": "11.0.3" + "uuid": "11.0.3", + "yarn": "^1.22.22" }, "devDependencies": { "eslint": "^8.0.1", @@ -1419,15 +1419,6 @@ "node": ">= 0.6" } }, - "node_modules/eventsource-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", - "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/express": { "version": "4.21.1", "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", @@ -3722,6 +3713,20 @@ "dev": true, "license": "ISC" }, + "node_modules/yarn": { + "version": "1.22.22", + "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz", + "integrity": "sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==", + "hasInstallScript": true, + "license": "BSD-2-Clause", + "bin": { + "yarn": "bin/yarn.js", + "yarnpkg": "bin/yarn.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 0741987..573961b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "body-parser": "1.20.3", "dotenv": "16.4.5", "express": "4.21.1", - "uuid": "11.0.3" + "uuid": "11.0.3", + "yarn": "^1.22.22" }, "scripts": { "start": "node src/index.js", diff --git a/process.go b/process.go new file mode 100644 index 0000000..c85c9e9 --- /dev/null +++ b/process.go @@ -0,0 +1,190 @@ +package main + +import ( + "bytes" + // "encoding/hex" + // "log" + "fmt" +) + + +// // 封装函数,用于将 chunk 转换为 UTF-8 字符串 +// function chunkToUtf8String (chunk) { +// if (chunk[0] === 0x01 || chunk[0] === 0x02 || (chunk[0] === 0x60 && chunk[1] === 0x0C)) { +// return '' +// } + +// console.log('chunk:', Buffer.from(chunk).toString('hex')) +// console.log('chunk string:', Buffer.from(chunk).toString('utf-8')) + +// // 去掉 chunk 中 0x0A 以及之前的字符 +// chunk = chunk.slice(chunk.indexOf(0x0A) + 1) + +// let filteredChunk = [] +// let i = 0 +// while (i < chunk.length) { +// // 新的条件过滤:如果遇到连续4个0x00,则移除其之后所有的以 0 开头的字节(0x00 到 0x0F) +// if (chunk.slice(i, i + 4).every(byte => byte === 0x00)) { +// i += 4 // 跳过这4个0x00 +// while (i < chunk.length && chunk[i] >= 0x00 && chunk[i] <= 0x0F) { +// i++ // 跳过所有以 0 开头的字节 +// } +// continue +// } + +// if (chunk[i] === 0x0C) { +// // 遇到 0x0C 时,跳过 0x0C 以及后续的所有连续的 0x0A +// i++ // 跳过 0x0C +// while (i < chunk.length && chunk[i] === 0x0A) { +// i++ // 跳过所有连续的 0x0A +// } +// } else if ( +// i > 0 && +// chunk[i] === 0x0A && +// chunk[i - 1] >= 0x00 && +// chunk[i - 1] <= 0x09 +// ) { +// // 如果当前字节是 0x0A,且前一个字节在 0x00 至 0x09 之间,跳过前一个字节和当前字节 +// filteredChunk.pop() // 移除已添加的前一个字节 +// i++ // 跳过当前的 0x0A +// } else { +// filteredChunk.push(chunk[i]) +// i++ +// } +// } + +// // 第二步:去除所有的 0x00 和 0x0C +// filteredChunk = filteredChunk.filter((byte) => byte !== 0x00 && byte !== 0x0C) + +// // 去除小于 0x0A 的字节 +// filteredChunk = filteredChunk.filter((byte) => byte >= 0x0A) + +// const hexString = Buffer.from(filteredChunk).toString('hex') +// console.log('hexString:', hexString) +// const utf8String = Buffer.from(filteredChunk).toString('utf-8') +// console.log('utf8String:', utf8String) +// return utf8String +// } +// func processChunk(chunk []byte) string { +// // 检查特殊字节开头的情况 +// if len(chunk) > 0 && (chunk[0] == 0x01 || chunk[0] == 0x02 || (len(chunk) > 1 && chunk[0] == 0x60 && chunk[1] == 0x0C)) { +// return "" +// } + +// // 打印调试信息 +// fmt.Printf("chunk: %x\n", chunk) +// fmt.Printf("chunk string: %s\n", string(chunk)) + +// // 找到第一个 0x0A 并截取之后的内容 +// index := bytes.IndexByte(chunk, 0x0A) +// if index != -1 { +// chunk = chunk[index+1:] +// } + +// // 创建过滤后的切片 +// filteredChunk := make([]byte, 0, len(chunk)) +// for i := 0; i < len(chunk); { +// // 检查连续4个0x00的情况 +// if i+4 <= len(chunk) { +// if chunk[i] == 0x00 && chunk[i+1] == 0x00 && chunk[i+2] == 0x00 && chunk[i+3] == 0x00 { +// i += 4 +// // 跳过所有以0开头的字节 +// for i < len(chunk) && chunk[i] <= 0x0F { +// i++ +// } +// continue +// } +// } + +// if chunk[i] == 0x0C { +// i++ +// // 跳过所有连续的0x0A +// for i < len(chunk) && chunk[i] == 0x0A { +// i++ +// } +// } else if i > 0 && chunk[i] == 0x0A && chunk[i-1] >= 0x00 && chunk[i-1] <= 0x09 { +// // 移除前一个字节并跳过当前的0x0A +// filteredChunk = filteredChunk[:len(filteredChunk)-1] +// i++ +// } else { +// filteredChunk = append(filteredChunk, chunk[i]) +// i++ +// } +// } + +// // 过滤掉0x00和0x0C +// tempChunk := make([]byte, 0, len(filteredChunk)) +// for _, b := range filteredChunk { +// if b != 0x00 && b != 0x0C { +// tempChunk = append(tempChunk, b) +// } +// } +// filteredChunk = tempChunk + +// // 过滤掉小于0x0A的字节 +// tempChunk = make([]byte, 0, len(filteredChunk)) +// for _, b := range filteredChunk { +// if b >= 0x0A { +// tempChunk = append(tempChunk, b) +// } +// } +// filteredChunk = tempChunk + +// // 打印调试信息并返回结果 +// fmt.Printf("hexString: %x\n", filteredChunk) +// result := string(filteredChunk) +// fmt.Printf("utf8String: %s\n", result) +// return result +// } + +func processChunk(chunk []byte) string { + // 检查特殊字节开头的情况 + if len(chunk) > 0 && (chunk[0] == 0x01 || chunk[0] == 0x02 || (len(chunk) > 1 && chunk[0] == 0x60 && chunk[1] == 0x0C)) { + return "" + } + + // 打印调试信息 + fmt.Printf("chunk: %x\n", chunk) + fmt.Printf("chunk string: %s\n", string(chunk)) + + // 找到第一个 0x0A 并截取之后的内容 + index := bytes.IndexByte(chunk, 0x0A) + if index != -1 { + chunk = chunk[index+1:] + } + + // 创建过滤后的切片 + filteredChunk := make([]byte, 0, len(chunk)) + for i := 0; i < len(chunk); { + // 检查连续4个0x00的情况 + if i+4 <= len(chunk) { + allZeros := true + for j := 0; j < 4; j++ { + if chunk[i+j] != 0x00 { + allZeros = false + break + } + } + if allZeros { + i += 4 + // 跳过所有以0开头的字节 + for i < len(chunk) && chunk[i] <= 0x0F { + i++ + } + continue + } + } + + // 保留UTF-8字符 + if chunk[i] >= 0xE0 || (chunk[i] >= 0x20 && chunk[i] <= 0x7F) { + filteredChunk = append(filteredChunk, chunk[i]) + } + i++ + } + + // 打印调试信息并返回结果 + fmt.Printf("hexString: %x\n", filteredChunk) + result := string(filteredChunk) + fmt.Printf("utf8String: %s\n", result) + return string(chunk) +} \ No newline at end of file diff --git a/types.go b/types.go new file mode 100644 index 0000000..2385e17 --- /dev/null +++ b/types.go @@ -0,0 +1,45 @@ +package main + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type ChatRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + Stream bool `json:"stream"` +} + +type ChatResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []struct { + Index int `json:"index"` + Message struct { + Role string `json:"role"` + Content string `json:"content"` + } `json:"message"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage"` +} + +type StreamResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []struct { + Index int `json:"index"` + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` +} \ No newline at end of file diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..0077c24 --- /dev/null +++ b/utils.go @@ -0,0 +1,83 @@ +package main + +import ( + "bytes" + "encoding/hex" + "fmt" + "strings" +) + +func stringToHex(str, modelName string) []byte { + inputBytes := []byte(str) + byteLength := len(inputBytes) + + const ( + FIXED_HEADER = 2 + SEPARATOR = 1 + ) + + FIXED_SUFFIX_LENGTH := 0xA3 + len(modelName) + + // 计算文本长度字段 + var textLengthField1, textLengthFieldSize1 int + if byteLength < 128 { + textLengthField1 = byteLength + textLengthFieldSize1 = 1 + } else { + lowByte1 := (byteLength & 0x7F) | 0x80 + highByte1 := (byteLength >> 7) & 0xFF + textLengthField1 = (highByte1 << 8) | lowByte1 + textLengthFieldSize1 = 2 + } + + // 计算基础长度 + baseLength := byteLength + 0x2A + var textLengthField, textLengthFieldSize int + if baseLength < 128 { + textLengthField = baseLength + textLengthFieldSize = 1 + } else { + lowByte := (baseLength & 0x7F) | 0x80 + highByte := (baseLength >> 7) & 0xFF + textLengthField = (highByte << 8) | lowByte + textLengthFieldSize = 2 + } + + // 计算总消息长度 + messageTotalLength := FIXED_HEADER + textLengthFieldSize + SEPARATOR + + textLengthFieldSize1 + byteLength + FIXED_SUFFIX_LENGTH + + var buf bytes.Buffer + + // 写入消息长度 + fmt.Fprintf(&buf, "%010x", messageTotalLength) + + // 写入固定头部 + buf.WriteString("12") + + // 写入长度字段 + fmt.Fprintf(&buf, "%02x", textLengthField) + + buf.WriteString("0A") + fmt.Fprintf(&buf, "%02x", textLengthField1) + + // 写入消息内容 + buf.WriteString(hex.EncodeToString(inputBytes)) + + // 写入固定后缀 + buf.WriteString("10016A2432343163636435662D393162612D343131382D393239612D3936626330313631626432612") + buf.WriteString("2002A132F643A2F6964656150726F2F656475626F73733A1E0A") + + // 写入模型名称长度和内容 + fmt.Fprintf(&buf, "%02X", len(modelName)) + buf.WriteString(strings.ToUpper(hex.EncodeToString([]byte(modelName)))) + + // 写入剩余固定内容 + buf.WriteString("22004A") + buf.WriteString("2461383761396133342D323164642D343863372D623434662D616636633365636536663765") + buf.WriteString("680070007A2436393337376535612D386332642D343835342D623564392D653062623232336163303061") + buf.WriteString("800101B00100C00100E00100E80100") + + hexBytes, _ := hex.DecodeString(strings.ToUpper(buf.String())) + return hexBytes +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index db6a059..64f7a7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -102,7 +102,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.9.0: +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.9.0: version "8.14.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== @@ -380,13 +380,6 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" @@ -394,13 +387,27 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.1, debug@^4.3.2: +debug@^4.3.1: version "4.3.7" resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: ms "^2.1.3" +debug@^4.3.2: + version "4.3.7" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -676,7 +683,7 @@ eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.0.1: +"eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^7.0.0 || ^8.0.0 || ^9.0.0", eslint@^8.0.1, eslint@>=6.0.0, eslint@>=7.0.0, eslint@>=8: version "8.57.1" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== @@ -1374,16 +1381,16 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +ms@^2.1.1, ms@^2.1.3, ms@2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== -ms@2.1.3, ms@^2.1.1, ms@^2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" @@ -1909,7 +1916,7 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -unpipe@1.0.0, unpipe@~1.0.0: +unpipe@~1.0.0, unpipe@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -2003,6 +2010,11 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +yarn@^1.22.22: + version "1.22.22" + resolved "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz" + integrity sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"