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"