diff --git a/.gitignore b/.gitignore index 553f947..62ed5f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ .env tests/ -node_modules/ \ No newline at end of file +node_modules/ +.DS_Store +.idea/ +__pycache__/ \ No newline at end of file diff --git a/api.go b/api.go deleted file mode 100644 index 3509480..0000000 --- a/api.go +++ /dev/null @@ -1,49 +0,0 @@ -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/cmd/server/main.go b/cmd/server/main.go deleted file mode 100644 index d4332a5..0000000 --- a/cmd/server/main.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "log" - "os" - - "github.com/joho/godotenv" - "cursor-api-proxy/internal/api" -) - -func main() { - if err := godotenv.Load(); err != nil { - log.Println("Warning: Error loading .env file") - } - - server := api.NewServer() - - port := os.Getenv("PORT") - if port == "" { - port = "3000" - } - - log.Printf("服务器运行在端口 %s\n", port) - server.Run(":" + port) -} \ No newline at end of file diff --git a/go.mod b/go.mod index 35f2184..9c66dc3 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,40 @@ -module cursor-api-proxy +module go-capi -go 1.21 +go 1.22.0 -// ... 其他依赖 ... +require ( + github.com/gin-contrib/cors v1.7.2 + github.com/gin-gonic/gin v1.10.0 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // 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.20.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.7 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // 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.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index a810f58..dadf5be 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,33 @@ -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/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= +github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= 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/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 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/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 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= @@ -34,57 +38,66 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA 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/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/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/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.5.0/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/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/handlers/chat.go b/handlers/chat.go new file mode 100644 index 0000000..d9db211 --- /dev/null +++ b/handlers/chat.go @@ -0,0 +1,265 @@ +package handlers + +import ( + "bytes" + "fmt" + "io" + "net/http" + "strings" + "time" + "bufio" + "encoding/json" + "unicode" + "regexp" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "go-capi/models" + "go-capi/utils" +) + +func ChatCompletions(c *gin.Context) { + var chatRequest models.ChatRequest + if err := c.ShouldBindJSON(&chatRequest); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 验证o1模型不支持流式输出 + if strings.HasPrefix(chatRequest.Model, "o1-") && chatRequest.Stream { + c.JSON(http.StatusBadRequest, gin.H{"error": "Model not supported stream"}) + return + } + + // 获取并处理认证令牌 + authHeader := c.GetHeader("Authorization") + if !strings.HasPrefix(authHeader, "Bearer ") { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header"}) + return + } + + authToken := strings.TrimPrefix(authHeader, "Bearer ") + if authToken == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing authorization token"}) + return + } + + // 处理多个密钥 + keys := strings.Split(authToken, ",") + if len(keys) > 0 { + authToken = strings.TrimSpace(keys[0]) + } + + if strings.Contains(authToken, "%3A%3A") { + parts := strings.Split(authToken, "%3A%3A") + authToken = parts[1] + } + + // 格式化消息 + var messages []string + for _, msg := range chatRequest.Messages { + messages = append(messages, fmt.Sprintf("%s:%s", msg.Role, msg.Content)) + } + formattedMessages := strings.Join(messages, "\n") + + // 生成请求数据 + hexData, err := utils.StringToHex(formattedMessages, chatRequest.Model) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 准备请求 + client := &http.Client{Timeout: 300 * time.Second} + req, err := http.NewRequest("POST", "https://api2.cursor.sh/aiserver.v1.AiService/StreamChat", bytes.NewReader(hexData)) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 设置请求头 + req.Header.Set("Content-Type", "application/connect+proto") + req.Header.Set("Authorization", "Bearer "+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", "Root="+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") + // ... 设置其他请求头 + + + // 打印 请求头和请求体 + fmt.Printf("\nRequest Headers: %v\n", req.Header) + fmt.Printf("\nRequest Body: %x\n", hexData) + + resp, err := client.Do(req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + defer resp.Body.Close() + + if chatRequest.Stream { + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + + chunks := make([][]byte, 0) + reader := bufio.NewReader(resp.Body) + + for { + chunk, err := reader.ReadBytes('\n') + if err == io.EOF { + break + } + if err != nil { + c.SSEvent("error", gin.H{"error": err.Error()}) + return + } + chunks = append(chunks, chunk) + } + + responseID := "chatcmpl-" + uuid.New().String() + c.Stream(func(w io.Writer) bool { + for _, chunk := range chunks { + text := chunkToUTF8String(chunk) + if text == "" { + continue + } + + // 清理文本 + text = strings.TrimSpace(text) + if strings.Contains(text, "<|END_USER|>") { + parts := strings.Split(text, "<|END_USER|>") + text = strings.TrimSpace(parts[len(parts)-1]) + } + if len(text) > 0 && unicode.IsLetter(rune(text[0])) { + text = strings.TrimSpace(text[1:]) + } + text = cleanControlChars(text) + + if text != "" { + dataBody := map[string]interface{}{ + "id": responseID, + "object": "chat.completion.chunk", + "created": time.Now().Unix(), + "choices": []map[string]interface{}{ + { + "index": 0, + "delta": map[string]string{ + "content": text, + }, + }, + }, + } + + jsonData, _ := json.Marshal(dataBody) + c.SSEvent("", string(jsonData)) + w.(http.Flusher).Flush() + } + } + + c.SSEvent("", "[DONE]") + return false + }) + } else { + // 非流式响应处理 + reader := bufio.NewReader(resp.Body) + var allText string + + for { + chunk, err := reader.ReadBytes('\n') + if err == io.EOF { + break + } + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + text := utils.ChunkToUTF8String(chunk) + if text != "" { + allText += text + } + } + + // 清理响应文本 + allText = cleanResponseText(allText) + + + response := models.ChatResponse{ + ID: "chatcmpl-" + uuid.New().String(), + Object: "chat.completion", + Created: time.Now().Unix(), + Model: chatRequest.Model, + Choices: []models.Choice{ + { + Index: 0, + Message: &models.Message{ + Role: "assistant", + Content: allText, + }, + FinishReason: "stop", + }, + }, + Usage: &models.Usage{ + PromptTokens: 0, + CompletionTokens: 0, + TotalTokens: 0, + }, + } + + c.JSON(http.StatusOK, response) + } +} + +// 辅助函数 +func chunkToUTF8String(chunk []byte) string { + // 实现从二进制chunk转换到UTF8字符串的逻辑 + return string(chunk) +} + +func cleanControlChars(text string) string { + return regexp.MustCompile(`[\x00-\x1F\x7F]`).ReplaceAllString(text, "") +} + +func cleanResponseText(text string) string { + // 移除END_USER之前的所有内容 + re := regexp.MustCompile(`(?s)^.*<\|END_USER\|>`) + text = re.ReplaceAllString(text, "") + + // 移除开头的换行和单个字母 + text = regexp.MustCompile(`^\n[a-zA-Z]?`).ReplaceAllString(text, "") + text = strings.TrimSpace(text) + + // 清理控制字符 + text = cleanControlChars(text) + + return text +} + +func GetModels(c *gin.Context) { + response := models.ModelsResponse{ + Object: "list", + Data: []models.ModelData{ + {ID: "claude-3-5-sonnet-20241022", Object: "model", Created: 1713744000, OwnedBy: "anthropic"}, + {ID: "claude-3-opus", Object: "model", Created: 1709251200, OwnedBy: "anthropic"}, + {ID: "claude-3.5-haiku", Object: "model", Created: 1711929600, OwnedBy: "anthropic"}, + {ID: "claude-3.5-sonnet", Object: "model", Created: 1711929600, OwnedBy: "anthropic"}, + {ID: "cursor-small", Object: "model", Created: 1712534400, OwnedBy: "cursor"}, + {ID: "gpt-3.5-turbo", Object: "model", Created: 1677649200, OwnedBy: "openai"}, + {ID: "gpt-4", Object: "model", Created: 1687392000, OwnedBy: "openai"}, + {ID: "gpt-4-turbo-2024-04-09", Object: "model", Created: 1712620800, OwnedBy: "openai"}, + {ID: "gpt-4o", Object: "model", Created: 1712620800, OwnedBy: "openai"}, + {ID: "gpt-4o-mini", Object: "model", Created: 1712620800, OwnedBy: "openai"}, + {ID: "o1-mini", Object: "model", Created: 1712620800, OwnedBy: "openai"}, + {ID: "o1-preview", Object: "model", Created: 1712620800, OwnedBy: "openai"}, + }, + } + c.JSON(http.StatusOK, response) +} \ No newline at end of file diff --git a/internal/api/handlers.go b/internal/api/handlers.go deleted file mode 100644 index 7ad70e6..0000000 --- a/internal/api/handlers.go +++ /dev/null @@ -1,33 +0,0 @@ -package api - -import ( - "net/http" - "strings" - - "github.com/gin-gonic/gin" - "cursor-api-proxy/internal/models" - "cursor-api-proxy/internal/service" -) - -func handleChat(c *gin.Context) { - var req models.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 - } - - // ... 其他处理逻辑 ... - - if req.Stream { - service.HandleStreamResponse(c, req) - return - } - - service.HandleNormalResponse(c, req) -} \ No newline at end of file diff --git a/internal/api/routes.go b/internal/api/routes.go deleted file mode 100644 index 20046b1..0000000 --- a/internal/api/routes.go +++ /dev/null @@ -1,15 +0,0 @@ -package api - -import ( - "github.com/gin-gonic/gin" -) - -func NewServer() *gin.Engine { - r := gin.Default() - setupRoutes(r) - return r -} - -func setupRoutes(r *gin.Engine) { - r.POST("/v1/chat/completions", handleChat) -} \ No newline at end of file diff --git a/internal/models/types.go b/internal/models/types.go deleted file mode 100644 index f6f6c3f..0000000 --- a/internal/models/types.go +++ /dev/null @@ -1,14 +0,0 @@ -package models - -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"` -} - -// ... 其他类型定义 ... \ No newline at end of file diff --git a/internal/service/chat.go b/internal/service/chat.go deleted file mode 100644 index 0ad206b..0000000 --- a/internal/service/chat.go +++ /dev/null @@ -1,15 +0,0 @@ -package service - -import ( - "github.com/gin-gonic/gin" - "cursor-api-proxy/internal/models" - "cursor-api-proxy/internal/utils" -) - -func HandleStreamResponse(c *gin.Context, req models.ChatRequest) { - // 从 handlers.go 移动流式响应处理逻辑到这里 -} - -func HandleNormalResponse(c *gin.Context, req models.ChatRequest) { - // 从 handlers.go 移动普通响应处理逻辑到这里 -} \ No newline at end of file diff --git a/internal/utils/hex.go b/internal/utils/hex.go deleted file mode 100644 index f66bf29..0000000 --- a/internal/utils/hex.go +++ /dev/null @@ -1,5 +0,0 @@ -package utils - -func StringToHex(str, modelName string) []byte { - // 从 utils.go 移动转换逻辑到这里 -} \ No newline at end of file diff --git a/internal/utils/process.go b/internal/utils/process.go deleted file mode 100644 index 6c8df2d..0000000 --- a/internal/utils/process.go +++ /dev/null @@ -1,5 +0,0 @@ -package utils - -func ProcessChunk(chunk []byte) string { - // 从 process.go 移动处理逻辑到这里 -} \ No newline at end of file diff --git a/main.go b/main.go index 830ca0c..e566299 100644 --- a/main.go +++ b/main.go @@ -1,97 +1,35 @@ package main import ( - "log" - "net/http" - "os" - "strings" + "go-capi/handlers" + + "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/joho/godotenv" ) -// 在 main() 函数之前添加以下结构体定义 -type ChatRequest struct { - Model string `json:"model"` - Messages []Message `json:"messages"` - Stream bool `json:"stream"` -} - -type Message struct { - Role string `json:"role"` - Content string `json:"content"` -} - func main() { - if err := godotenv.Load(); err != nil { - log.Println("Warning: Error loading .env file") - } + // 加载环境变量 + godotenv.Load() r := gin.Default() - r.POST("/v1/chat/completions", handleChat) - port := os.Getenv("PORT") - if port == "" { - port = "3000" - } - - log.Printf("服务器运行在端口 %s\n", port) + // 配置CORS + r.Use(cors.New(cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST",}, + AllowHeaders: []string{"*"}, + AllowCredentials: true, + })) + + // 注册路由 + r.POST("/v1/chat/completions", handlers.ChatCompletions) + r.GET("/models", handlers.GetModels) + + // 获取端口号 + // port := os.Getenv("PORT") + port := "3001" + 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) -} - -// 在文件末尾添加这两个新函数 -func handleStreamResponse(c *gin.Context, req ChatRequest) { - // TODO: 实现流式响应的逻辑 - c.JSON(http.StatusNotImplemented, gin.H{ - "error": "Stream response not implemented yet", - }) -} - -func handleNormalResponse(c *gin.Context, req ChatRequest) { - // TODO: 实现普通响应的逻辑 - c.JSON(http.StatusNotImplemented, gin.H{ - "error": "Normal response not implemented yet", - }) -} \ No newline at end of file +} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..93ff7c7 --- /dev/null +++ b/main.py @@ -0,0 +1,411 @@ +import json + +from fastapi import FastAPI, Request, Response, HTTPException +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import List, Optional, Dict, Any +import uuid +import httpx +import os +from dotenv import load_dotenv +import time +import re + +# 加载环境变量 +load_dotenv() + +app = FastAPI() + +# 添加CORS中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# 定义请求模型 +class Message(BaseModel): + role: str + content: str + + +class ChatRequest(BaseModel): + model: str + messages: List[Message] + stream: bool = False + + +def string_to_hex(text: str, model_name: str) -> bytes: + """将文本转换为特定格式的十六进制数据""" + # 将输入文本转换为UTF-8字节 + text_bytes = text.encode('utf-8') + text_length = len(text_bytes) + + # 固定常量 + FIXED_HEADER = 2 + SEPARATOR = 1 + FIXED_SUFFIX_LENGTH = 0xA3 + len(model_name) + + # 计算第一个长度字段 + if text_length < 128: + text_length_field1 = format(text_length, '02x') + text_length_field_size1 = 1 + else: + low_byte1 = (text_length & 0x7F) | 0x80 + high_byte1 = (text_length >> 7) & 0xFF + text_length_field1 = format(low_byte1, '02x') + format(high_byte1, '02x') + text_length_field_size1 = 2 + + # 计算基础长度字段 + base_length = text_length + 0x2A + if base_length < 128: + text_length_field = format(base_length, '02x') + text_length_field_size = 1 + else: + low_byte = (base_length & 0x7F) | 0x80 + high_byte = (base_length >> 7) & 0xFF + text_length_field = format(low_byte, '02x') + format(high_byte, '02x') + text_length_field_size = 2 + + # 计算总消息长度 + message_total_length = (FIXED_HEADER + text_length_field_size + SEPARATOR + + text_length_field_size1 + text_length + FIXED_SUFFIX_LENGTH) + + # 构造十六进制字符串 + model_name_bytes = model_name.encode('utf-8') + model_name_length_hex = format(len(model_name_bytes), '02X') + model_name_hex = model_name_bytes.hex().upper() + + hex_string = ( + f"{message_total_length:010x}" + "12" + f"{text_length_field}" + "0A" + f"{text_length_field1}" + f"{text_bytes.hex()}" + "10016A2432343163636435662D393162612D343131382D393239612D3936626330313631626432612" + "2002A132F643A2F6964656150726F2F656475626F73733A1E0A" + f"{model_name_length_hex}" + f"{model_name_hex}" + "22004A" + "2461383761396133342D323164642D343863372D623434662D616636633365636536663765" + "680070007A2436393337376535612D386332642D343835342D623564392D653062623232336163303061" + "800101B00100C00100E00100E80100" + ).upper() + + return bytes.fromhex(hex_string) + + +def chunk_to_utf8_string(chunk: bytes) -> str: + """将二进制chunk转换为UTF-8字符串""" + if not chunk or len(chunk) < 2: + return '' + + if chunk[0] in [0x01, 0x02] or (chunk[0] == 0x60 and chunk[1] == 0x0C): + return '' + + # 记录原始chunk的十六进制(调试用) + print(f"chunk length: {len(chunk)}") + # print(f"chunk hex: {chunk.hex()}") + + try: + # 去掉0x0A之前的所有字节 + try: + chunk = chunk[chunk.index(0x0A) + 1:] + except ValueError: + pass + + filtered_chunk = bytearray() + i = 0 + while i < len(chunk): + # 检查是否有连续的0x00 + if i + 4 <= len(chunk) and all(chunk[j] == 0x00 for j in range(i, i + 4)): + i += 4 + while i < len(chunk) and chunk[i] <= 0x0F: + i += 1 + continue + + if chunk[i] == 0x0C: + i += 1 + while i < len(chunk) and chunk[i] == 0x0A: + i += 1 + else: + filtered_chunk.append(chunk[i]) + i += 1 + + # 过滤掉特定字节 + filtered_chunk = bytes(b for b in filtered_chunk + if b != 0x00 and b != 0x0C) + + if not filtered_chunk: + return '' + + result = filtered_chunk.decode('utf-8', errors='ignore').strip() + # print(f"decoded result: {result}") # 调试输出 + return result + + except Exception as e: + print(f"Error in chunk_to_utf8_string: {str(e)}") + return '' + + +async def process_stream(chunks, ): + """处理流式响应""" + response_id = f"chatcmpl-{str(uuid.uuid4())}" + + # 先将所有chunks读取到列表中 + # chunks = [] + # async for chunk in response.aiter_raw(): + # chunks.append(chunk) + + # 然后处理保存的chunks + for chunk in chunks: + text = chunk_to_utf8_string(chunk) + if text: + # 清理文本 + text = text.strip() + if "<|END_USER|>" in text: + text = text.split("<|END_USER|>")[-1].strip() + if text and text[0].isalpha(): + text = text[1:].strip() + text = re.sub(r"[\x00-\x1F\x7F]", "", text) + + if text: # 确保清理后的文本不为空 + data_body = { + "id": response_id, + "object": "chat.completion.chunk", + "created": int(time.time()), + "choices": [{ + "index": 0, + "delta": { + "content": text + } + }] + } + yield f"data: {json.dumps(data_body, ensure_ascii=False)}\n\n" + # yield "data: {\n" + # yield f' "id": "{response_id}",\n' + # yield ' "object": "chat.completion.chunk",\n' + # yield f' "created": {int(time.time())},\n' + # yield ' "choices": [{\n' + # yield ' "index": 0,\n' + # yield ' "delta": {\n' + # yield f' "content": "{text}"\n' + # yield " }\n" + # yield " }]\n" + # yield "}\n\n" + + yield "data: [DONE]\n\n" + + +@app.post("/v1/chat/completions") +async def chat_completions(request: Request, chat_request: ChatRequest): + # 验证o1模型不支持流式输出 + if chat_request.model.startswith('o1-') and chat_request.stream: + raise HTTPException( + status_code=400, + detail="Model not supported stream" + ) + + # 获取并处理认证令牌 + auth_header = request.headers.get('authorization', '') + if not auth_header.startswith('Bearer '): + raise HTTPException( + status_code=401, + detail="Invalid authorization header" + ) + + auth_token = auth_header.replace('Bearer ', '') + if not auth_token: + raise HTTPException( + status_code=401, + detail="Missing authorization token" + ) + + # 处理多个密钥 + keys = [key.strip() for key in auth_token.split(',')] + if keys: + auth_token = keys[0] # 使用第一个密钥 + + if '%3A%3A' in auth_token: + auth_token = auth_token.split('%3A%3A')[1] + + # 格式化消息 + formatted_messages = "\n".join( + f"{msg.role}:{msg.content}" for msg in chat_request.messages + ) + + # 生成请求数据 + hex_data = string_to_hex(formatted_messages, chat_request.model) + + # 准备请求头 + headers = { + 'Content-Type': 'application/connect+proto', + 'Authorization': f'Bearer {auth_token}', + 'Connect-Accept-Encoding': 'gzip,br', + 'Connect-Protocol-Version': '1', + 'User-Agent': 'connect-es/1.4.0', + 'X-Amzn-Trace-Id': f'Root={str(uuid.uuid4())}', + 'X-Cursor-Checksum': 'zo6Qjequ9b9734d1f13c3438ba25ea31ac93d9287248b9d30434934e9fcbfa6b3b22029e/7e4af391f67188693b722eff0090e8e6608bca8fa320ef20a0ccb5d7d62dfdef', + 'X-Cursor-Client-Version': '0.42.3', + 'X-Cursor-Timezone': 'Asia/Shanghai', + 'X-Ghost-Mode': 'false', + 'X-Request-Id': str(uuid.uuid4()), + 'Host': 'api2.cursor.sh' + } + + async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client: + try: + # 使用 stream=True 参数 + # 打印 headers 和 二进制 data + print(f"headers: {headers}") + print(hex_data) + async with client.stream( + 'POST', + 'https://api2.cursor.sh/aiserver.v1.AiService/StreamChat', + headers=headers, + content=hex_data, + timeout=None + ) as response: + if chat_request.stream: + chunks = [] + async for chunk in response.aiter_raw(): + chunks.append(chunk) + return StreamingResponse( + process_stream(chunks), + media_type="text/event-stream" + ) + else: + # 非流式响应处理 + text = '' + async for chunk in response.aiter_raw(): + # print('chunk:', chunk.hex()) + print('chunk length:', len(chunk)) + + res = chunk_to_utf8_string(chunk) + # print('res:', res) + if res: + text += res + + # 清理响应文本 + import re + text = re.sub(r'^.*<\|END_USER\|>', '', text, flags=re.DOTALL) + text = re.sub(r'^\n[a-zA-Z]?', '', text).strip() + text = re.sub(r'[\x00-\x1F\x7F]', '', text) + + return { + "id": f"chatcmpl-{str(uuid.uuid4())}", + "object": "chat.completion", + "created": int(time.time()), + "model": chat_request.model, + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": text + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0 + } + } + + except Exception as e: + print(f"Error: {str(e)}") + raise HTTPException( + status_code=500, + detail="Internal server error" + ) + + +@app.post("/models") +async def models(): + return { + "object": "list", + "data": [ + { + "id": "claude-3-5-sonnet-20241022", + "object": "model", + "created": 1713744000, + "owned_by": "anthropic" + }, + { + "id": "claude-3-opus", + "object": "model", + "created": 1709251200, + "owned_by": "anthropic" + }, + { + "id": "claude-3.5-haiku", + "object": "model", + "created": 1711929600, + "owned_by": "anthropic" + }, + { + "id": "claude-3.5-sonnet", + "object": "model", + "created": 1711929600, + "owned_by": "anthropic" + }, + { + "id": "cursor-small", + "object": "model", + "created": 1712534400, + "owned_by": "cursor" + }, + { + "id": "gpt-3.5-turbo", + "object": "model", + "created": 1677649200, + "owned_by": "openai" + }, + { + "id": "gpt-4", + "object": "model", + "created": 1687392000, + "owned_by": "openai" + }, + { + "id": "gpt-4-turbo-2024-04-09", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "gpt-4o", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "gpt-4o-mini", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "o1-mini", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "o1-preview", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + } + ] + } + +if __name__ == "__main__": + import uvicorn + port = int(os.getenv("PORT", "3001")) + uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True, timeout_keep_alive=30) diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..8f97bc8 --- /dev/null +++ b/models/models.go @@ -0,0 +1,50 @@ +package models + +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,omitempty"` +} + +type Delta struct { + Content string `json:"content"` +} + +type Choice struct { + Index int `json:"index"` + Delta Delta `json:"delta,omitempty"` + Message *Message `json:"message,omitempty"` + FinishReason string `json:"finish_reason,omitempty"` +} + +type ChatResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model,omitempty"` + Choices []Choice `json:"choices"` + Usage *Usage `json:"usage,omitempty"` +} + +type Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` +} + +type ModelData struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + OwnedBy string `json:"owned_by"` +} + +type ModelsResponse struct { + Object string `json:"object"` + Data []ModelData `json:"data"` +} \ No newline at end of file diff --git a/process.go b/process.go deleted file mode 100644 index c85c9e9..0000000 --- a/process.go +++ /dev/null @@ -1,190 +0,0 @@ -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/readme.md b/readme.md index e0ca4a7..5135111 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,3 @@ -# cursor-api - -将 Cursor 编辑器转换为 OpenAI 兼容的 API 接口服务。 ## 项目简介 diff --git a/rs-capi/.vscode/launch.json b/rs-capi/.vscode/launch.json new file mode 100644 index 0000000..acb9148 --- /dev/null +++ b/rs-capi/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'rs-api'", + "cargo": { + "args": [ + "build", + "--bin=rs-api", + "--package=rs-api" + ], + "filter": { + "name": "rs-api", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'rs-api'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=rs-api", + "--package=rs-api" + ], + "filter": { + "name": "rs-api", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/rs-capi/Cargo.lock b/rs-capi/Cargo.lock new file mode 100644 index 0000000..2fd5fff --- /dev/null +++ b/rs-capi/Cargo.lock @@ -0,0 +1,1965 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "cc" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.31", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.5.1", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.165" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb4d3d38eab6c5239a362fa8bae48c03baf980a6e7079f063942d563ef3533e" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.31", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg", +] + +[[package]] +name = "rs-api" +version = "0.1.0" +dependencies = [ + "axum", + "bytes", + "chrono", + "dotenv", + "futures", + "hex", + "http 1.1.0", + "hyper 1.5.1", + "regex", + "reqwest", + "serde", + "serde_json", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "syn" +version = "2.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/rs-capi/Cargo.toml b/rs-capi/Cargo.toml new file mode 100644 index 0000000..2c2bb78 --- /dev/null +++ b/rs-capi/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "rs-api" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = { version = "0.7", features = ["json"] } +tokio = { version = "1.0", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +reqwest = { version = "0.11", features = ["json", "stream"] } +tower-http = { version = "0.5", features = ["cors", "trace"] } +uuid = { version = "1.0", features = ["v4"] } +dotenv = "0.15" +chrono = "0.4" +futures = "0.3" +bytes = "1.0" +regex = "1.5" +tracing = "0.1" +tracing-subscriber = "0.3" +hex = "0.4" +hyper = "1.5.1" +http = "1.1.0" diff --git a/rs-capi/main.py b/rs-capi/main.py new file mode 100644 index 0000000..30ccc01 --- /dev/null +++ b/rs-capi/main.py @@ -0,0 +1,822 @@ +import json + +from fastapi import FastAPI, Request, Response, HTTPException +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import List, Optional, Dict, Any +import uuid +import httpx +import os +from dotenv import load_dotenv +import time +import re + +# 加载环境变量 +load_dotenv() + +app = FastAPI() + +# 添加CORS中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# 定义请求模型 +class Message(BaseModel): + role: str + content: str + + +class ChatRequest(BaseModel): + model: str + messages: List[Message] + stream: bool = False + + +def string_to_hex(text: str, model_name: str) -> bytes: + """将文本转换为特定格式的十六进制数据""" + # 将输入文本转换为UTF-8字节 + text_bytes = text.encode('utf-8') + text_length = len(text_bytes) + + # 固定常量 + FIXED_HEADER = 2 + SEPARATOR = 1 + FIXED_SUFFIX_LENGTH = 0xA3 + len(model_name) + + # 计算第一个长度字段 + if text_length < 128: + text_length_field1 = format(text_length, '02x') + text_length_field_size1 = 1 + else: + low_byte1 = (text_length & 0x7F) | 0x80 + high_byte1 = (text_length >> 7) & 0xFF + text_length_field1 = format(low_byte1, '02x') + format(high_byte1, '02x') + text_length_field_size1 = 2 + + # 计算基础长度字段 + base_length = text_length + 0x2A + if base_length < 128: + text_length_field = format(base_length, '02x') + text_length_field_size = 1 + else: + low_byte = (base_length & 0x7F) | 0x80 + high_byte = (base_length >> 7) & 0xFF + text_length_field = format(low_byte, '02x') + format(high_byte, '02x') + text_length_field_size = 2 + + # 计算总消息长度 + message_total_length = (FIXED_HEADER + text_length_field_size + SEPARATOR + + text_length_field_size1 + text_length + FIXED_SUFFIX_LENGTH) + + # 构造十六进制字符串 + model_name_bytes = model_name.encode('utf-8') + model_name_length_hex = format(len(model_name_bytes), '02X') + model_name_hex = model_name_bytes.hex().upper() + + hex_string = ( + f"{message_total_length:010x}" + "12" + f"{text_length_field}" + "0A" + f"{text_length_field1}" + f"{text_bytes.hex()}" + "10016A2432343163636435662D393162612D343131382D393239612D3936626330313631626432612" + "2002A132F643A2F6964656150726F2F656475626F73733A1E0A" + f"{model_name_length_hex}" + f"{model_name_hex}" + "22004A" + "2461383761396133342D323164642D343863372D623434662D616636633365636536663765" + "680070007A2436393337376535612D386332642D343835342D623564392D653062623232336163303061" + "800101B00100C00100E00100E80100" + ).upper() + + return bytes.fromhex(hex_string) + + +def chunk_to_utf8_string(chunk: bytes) -> str: + """将二进制chunk转换为UTF-8字符串""" + if not chunk or len(chunk) < 2: + return '' + + if chunk[0] in [0x01, 0x02] or (chunk[0] == 0x60 and chunk[1] == 0x0C): + return '' + + # 记录原始chunk的十六进制(调试用) + print(f"chunk length: {len(chunk)}") + # print(f"chunk hex: {chunk.hex()}") + + try: + # 去掉0x0A之前的所有字节 + try: + chunk = chunk[chunk.index(0x0A) + 1:] + except ValueError: + pass + + filtered_chunk = bytearray() + i = 0 + while i < len(chunk): + # 检查是否有连续的0x00 + if i + 4 <= len(chunk) and all(chunk[j] == 0x00 for j in range(i, i + 4)): + i += 4 + while i < len(chunk) and chunk[i] <= 0x0F: + i += 1 + continue + + if chunk[i] == 0x0C: + i += 1 + while i < len(chunk) and chunk[i] == 0x0A: + i += 1 + else: + filtered_chunk.append(chunk[i]) + i += 1 + + # 过滤掉特定字节 + filtered_chunk = bytes(b for b in filtered_chunk + if b != 0x00 and b != 0x0C) + + if not filtered_chunk: + return '' + + result = filtered_chunk.decode('utf-8', errors='ignore').strip() + # print(f"decoded result: {result}") # 调试输出 + return result + + except Exception as e: + print(f"Error in chunk_to_utf8_string: {str(e)}") + return '' + + +async def process_stream(chunks, ): + """处理流式响应""" + response_id = f"chatcmpl-{str(uuid.uuid4())}" + + # 先将所有chunks读取到列表中 + # chunks = [] + # async for chunk in response.aiter_raw(): + # chunks.append(chunk) + + # 然后处理保存的chunks + for chunk in chunks: + text = chunk_to_utf8_string(chunk) + if text: + # 清理文本 + text = text.strip() + if "<|END_USER|>" in text: + text = text.split("<|END_USER|>")[-1].strip() + if text and text[0].isalpha(): + text = text[1:].strip() + text = re.sub(r"[\x00-\x1F\x7F]", "", text) + + if text: # 确保清理后的文本不为空 + data_body = { + "id": response_id, + "object": "chat.completion.chunk", + "created": int(time.time()), + "choices": [{ + "index": 0, + "delta": { + "content": text + } + }] + } + yield f"data: {json.dumps(data_body, ensure_ascii=False)}\n\n" + # yield "data: {\n" + # yield f' "id": "{response_id}",\n' + # yield ' "object": "chat.completion.chunk",\n' + # yield f' "created": {int(time.time())},\n' + # yield ' "choices": [{\n' + # yield ' "index": 0,\n' + # yield ' "delta": {\n' + # yield f' "content": "{text}"\n' + # yield " }\n" + # yield " }]\n" + # yield "}\n\n" + + yield "data: [DONE]\n\n" + + +@app.post("/v1/chat/completions") +async def chat_completions(request: Request, chat_request: ChatRequest): + # 验证o1模型不支持流式输出 + if chat_request.model.startswith('o1-') and chat_request.stream: + raise HTTPException( + status_code=400, + detail="Model not supported stream" + ) + + # 获取并处理认证令牌 + auth_header = request.headers.get('authorization', '') + if not auth_header.startswith('Bearer '): + raise HTTPException( + status_code=401, + detail="Invalid authorization header" + ) + + auth_token = auth_header.replace('Bearer ', '') + if not auth_token: + raise HTTPException( + status_code=401, + detail="Missing authorization token" + ) + + # 处理多个密钥 + keys = [key.strip() for key in auth_token.split(',')] + if keys: + auth_token = keys[0] # 使用第一个密钥 + + if '%3A%3A' in auth_token: + auth_token = auth_token.split('%3A%3A')[1] + + # 格式化消息 + formatted_messages = "\n".join( + f"{msg.role}:{msg.content}" for msg in chat_request.messages + ) + + # 生成请求数据 + hex_data = string_to_hex(formatted_messages, chat_request.model) + + # 准备请求头 + headers = { + 'Content-Type': 'application/connect+proto', + 'Authorization': f'Bearer {auth_token}', + 'Connect-Accept-Encoding': 'gzip,br', + 'Connect-Protocol-Version': '1', + 'User-Agent': 'connect-es/1.4.0', + 'X-Amzn-Trace-Id': f'Root={str(uuid.uuid4())}', + 'X-Cursor-Checksum': 'zo6Qjequ9b9734d1f13c3438ba25ea31ac93d9287248b9d30434934e9fcbfa6b3b22029e/7e4af391f67188693b722eff0090e8e6608bca8fa320ef20a0ccb5d7d62dfdef', + 'X-Cursor-Client-Version': '0.42.3', + 'X-Cursor-Timezone': 'Asia/Shanghai', + 'X-Ghost-Mode': 'false', + 'X-Request-Id': str(uuid.uuid4()), + 'Host': 'api2.cursor.sh' + } + + async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client: + try: + # 使用 stream=True 参数 + # 打印 headers 和 二进制 data + print(f"headers: {headers}") + print(hex_data) + async with client.stream( + 'POST', + 'https://api2.cursor.sh/aiserver.v1.AiService/StreamChat', + headers=headers, + content=hex_data, + timeout=None + ) as response: + if chat_request.stream: + chunks = [] + async for chunk in response.aiter_raw(): + chunks.append(chunk) + return StreamingResponse( + process_stream(chunks), + media_type="text/event-stream" + ) + else: + # 非流式响应处理 + text = '' + async for chunk in response.aiter_raw(): + # print('chunk:', chunk.hex()) + print('chunk length:', len(chunk)) + + res = chunk_to_utf8_string(chunk) + # print('res:', res) + if res: + text += res + + # 清理响应文本 + import re + text = re.sub(r'^.*<\|END_USER\|>', '', text, flags=re.DOTALL) + text = re.sub(r'^\n[a-zA-Z]?', '', text).strip() + text = re.sub(r'[\x00-\x1F\x7F]', '', text) + + return { + "id": f"chatcmpl-{str(uuid.uuid4())}", + "object": "chat.completion", + "created": int(time.time()), + "model": chat_request.model, + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": text + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0 + } + } + + except Exception as e: + print(f"Error: {str(e)}") + raise HTTPException( + status_code=500, + detail="Internal server error" + ) + + +@app.post("/models") +async def models(): + return { + "object": "list", + "data": [ + { + "id": "claude-3-5-sonnet-20241022", + "object": "model", + "created": 1713744000, + "owned_by": "anthropic" + }, + { + "id": "claude-3-opus", + "object": "model", + "created": 1709251200, + "owned_by": "anthropic" + }, + { + "id": "claude-3.5-haiku", + "object": "model", + "created": 1711929600, + "owned_by": "anthropic" + }, + { + "id": "claude-3.5-sonnet", + "object": "model", + "created": 1711929600, + "owned_by": "anthropic" + }, + { + "id": "cursor-small", + "object": "model", + "created": 1712534400, + "owned_by": "cursor" + }, + { + "id": "gpt-3.5-turbo", + "object": "model", + "created": 1677649200, + "owned_by": "openai" + }, + { + "id": "gpt-4", + "object": "model", + "created": 1687392000, + "owned_by": "openai" + }, + { + "id": "gpt-4-turbo-2024-04-09", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "gpt-4o", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "gpt-4o-mini", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "o1-mini", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "o1-preview", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + } + ] + } + +if __name__ == "__main__": + import uvicorn + port = int(os.getenv("PORT", "3001")) + uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True, timeout_keep_alive=30) +import json + +from fastapi import FastAPI, Request, Response, HTTPException +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from typing import List, Optional, Dict, Any +import uuid +import httpx +import os +from dotenv import load_dotenv +import time +import re + +# 加载环境变量 +load_dotenv() + +app = FastAPI() + +# 添加CORS中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# 定义请求模型 +class Message(BaseModel): + role: str + content: str + + +class ChatRequest(BaseModel): + model: str + messages: List[Message] + stream: bool = False + + +def string_to_hex(text: str, model_name: str) -> bytes: + """将文本转换为特定格式的十六进制数据""" + # 将输入文本转换为UTF-8字节 + text_bytes = text.encode('utf-8') + text_length = len(text_bytes) + + # 固定常量 + FIXED_HEADER = 2 + SEPARATOR = 1 + FIXED_SUFFIX_LENGTH = 0xA3 + len(model_name) + + # 计算第一个长度字段 + if text_length < 128: + text_length_field1 = format(text_length, '02x') + text_length_field_size1 = 1 + else: + low_byte1 = (text_length & 0x7F) | 0x80 + high_byte1 = (text_length >> 7) & 0xFF + text_length_field1 = format(low_byte1, '02x') + format(high_byte1, '02x') + text_length_field_size1 = 2 + + # 计算基础长度字段 + base_length = text_length + 0x2A + if base_length < 128: + text_length_field = format(base_length, '02x') + text_length_field_size = 1 + else: + low_byte = (base_length & 0x7F) | 0x80 + high_byte = (base_length >> 7) & 0xFF + text_length_field = format(low_byte, '02x') + format(high_byte, '02x') + text_length_field_size = 2 + + # 计算总消息长度 + message_total_length = (FIXED_HEADER + text_length_field_size + SEPARATOR + + text_length_field_size1 + text_length + FIXED_SUFFIX_LENGTH) + + # 构造十六进制字符串 + model_name_bytes = model_name.encode('utf-8') + model_name_length_hex = format(len(model_name_bytes), '02X') + model_name_hex = model_name_bytes.hex().upper() + + hex_string = ( + f"{message_total_length:010x}" + "12" + f"{text_length_field}" + "0A" + f"{text_length_field1}" + f"{text_bytes.hex()}" + "10016A2432343163636435662D393162612D343131382D393239612D3936626330313631626432612" + "2002A132F643A2F6964656150726F2F656475626F73733A1E0A" + f"{model_name_length_hex}" + f"{model_name_hex}" + "22004A" + "2461383761396133342D323164642D343863372D623434662D616636633365636536663765" + "680070007A2436393337376535612D386332642D343835342D623564392D653062623232336163303061" + "800101B00100C00100E00100E80100" + ).upper() + + return bytes.fromhex(hex_string) + + +def chunk_to_utf8_string(chunk: bytes) -> str: + """将二进制chunk转换为UTF-8字符串""" + if not chunk or len(chunk) < 2: + return '' + + if chunk[0] in [0x01, 0x02] or (chunk[0] == 0x60 and chunk[1] == 0x0C): + return '' + + # 记录原始chunk的十六进制(调试用) + print(f"chunk length: {len(chunk)}") + # print(f"chunk hex: {chunk.hex()}") + + try: + # 去掉0x0A之前的所有字节 + try: + chunk = chunk[chunk.index(0x0A) + 1:] + except ValueError: + pass + + filtered_chunk = bytearray() + i = 0 + while i < len(chunk): + # 检查是否有连续的0x00 + if i + 4 <= len(chunk) and all(chunk[j] == 0x00 for j in range(i, i + 4)): + i += 4 + while i < len(chunk) and chunk[i] <= 0x0F: + i += 1 + continue + + if chunk[i] == 0x0C: + i += 1 + while i < len(chunk) and chunk[i] == 0x0A: + i += 1 + else: + filtered_chunk.append(chunk[i]) + i += 1 + + # 过滤掉特定字节 + filtered_chunk = bytes(b for b in filtered_chunk + if b != 0x00 and b != 0x0C) + + if not filtered_chunk: + return '' + + result = filtered_chunk.decode('utf-8', errors='ignore').strip() + # print(f"decoded result: {result}") # 调试输出 + return result + + except Exception as e: + print(f"Error in chunk_to_utf8_string: {str(e)}") + return '' + + +async def process_stream(chunks, ): + """处理流式响应""" + response_id = f"chatcmpl-{str(uuid.uuid4())}" + + # 先将所有chunks读取到列表中 + # chunks = [] + # async for chunk in response.aiter_raw(): + # chunks.append(chunk) + + # 然后处理保存的chunks + for chunk in chunks: + text = chunk_to_utf8_string(chunk) + if text: + # 清理文本 + text = text.strip() + if "<|END_USER|>" in text: + text = text.split("<|END_USER|>")[-1].strip() + if text and text[0].isalpha(): + text = text[1:].strip() + text = re.sub(r"[\x00-\x1F\x7F]", "", text) + + if text: # 确保清理后的文本不为空 + data_body = { + "id": response_id, + "object": "chat.completion.chunk", + "created": int(time.time()), + "choices": [{ + "index": 0, + "delta": { + "content": text + } + }] + } + yield f"data: {json.dumps(data_body, ensure_ascii=False)}\n\n" + # yield "data: {\n" + # yield f' "id": "{response_id}",\n' + # yield ' "object": "chat.completion.chunk",\n' + # yield f' "created": {int(time.time())},\n' + # yield ' "choices": [{\n' + # yield ' "index": 0,\n' + # yield ' "delta": {\n' + # yield f' "content": "{text}"\n' + # yield " }\n" + # yield " }]\n" + # yield "}\n\n" + + yield "data: [DONE]\n\n" + + +@app.post("/v1/chat/completions") +async def chat_completions(request: Request, chat_request: ChatRequest): + # 验证o1模型不支持流式输出 + if chat_request.model.startswith('o1-') and chat_request.stream: + raise HTTPException( + status_code=400, + detail="Model not supported stream" + ) + + # 获取并处理认证令牌 + auth_header = request.headers.get('authorization', '') + if not auth_header.startswith('Bearer '): + raise HTTPException( + status_code=401, + detail="Invalid authorization header" + ) + + auth_token = auth_header.replace('Bearer ', '') + if not auth_token: + raise HTTPException( + status_code=401, + detail="Missing authorization token" + ) + + # 处理多个密钥 + keys = [key.strip() for key in auth_token.split(',')] + if keys: + auth_token = keys[0] # 使用第一个密钥 + + if '%3A%3A' in auth_token: + auth_token = auth_token.split('%3A%3A')[1] + + # 格式化消息 + formatted_messages = "\n".join( + f"{msg.role}:{msg.content}" for msg in chat_request.messages + ) + + # 生成请求数据 + hex_data = string_to_hex(formatted_messages, chat_request.model) + + # 准备请求头 + headers = { + 'Content-Type': 'application/connect+proto', + 'Authorization': f'Bearer {auth_token}', + 'Connect-Accept-Encoding': 'gzip,br', + 'Connect-Protocol-Version': '1', + 'User-Agent': 'connect-es/1.4.0', + 'X-Amzn-Trace-Id': f'Root={str(uuid.uuid4())}', + 'X-Cursor-Checksum': 'zo6Qjequ9b9734d1f13c3438ba25ea31ac93d9287248b9d30434934e9fcbfa6b3b22029e/7e4af391f67188693b722eff0090e8e6608bca8fa320ef20a0ccb5d7d62dfdef', + 'X-Cursor-Client-Version': '0.42.3', + 'X-Cursor-Timezone': 'Asia/Shanghai', + 'X-Ghost-Mode': 'false', + 'X-Request-Id': str(uuid.uuid4()), + 'Host': 'api2.cursor.sh' + } + + async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client: + try: + # 使用 stream=True 参数 + # 打印 headers 和 二进制 data + print(f"headers: {headers}") + print(hex_data) + async with client.stream( + 'POST', + 'https://api2.cursor.sh/aiserver.v1.AiService/StreamChat', + headers=headers, + content=hex_data, + timeout=None + ) as response: + if chat_request.stream: + chunks = [] + async for chunk in response.aiter_raw(): + chunks.append(chunk) + return StreamingResponse( + process_stream(chunks), + media_type="text/event-stream" + ) + else: + # 非流式响应处理 + text = '' + async for chunk in response.aiter_raw(): + # print('chunk:', chunk.hex()) + print('chunk length:', len(chunk)) + + res = chunk_to_utf8_string(chunk) + # print('res:', res) + if res: + text += res + + # 清理响应文本 + import re + text = re.sub(r'^.*<\|END_USER\|>', '', text, flags=re.DOTALL) + text = re.sub(r'^\n[a-zA-Z]?', '', text).strip() + text = re.sub(r'[\x00-\x1F\x7F]', '', text) + + return { + "id": f"chatcmpl-{str(uuid.uuid4())}", + "object": "chat.completion", + "created": int(time.time()), + "model": chat_request.model, + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": text + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0 + } + } + + except Exception as e: + print(f"Error: {str(e)}") + raise HTTPException( + status_code=500, + detail="Internal server error" + ) + + +@app.post("/models") +async def models(): + return { + "object": "list", + "data": [ + { + "id": "claude-3-5-sonnet-20241022", + "object": "model", + "created": 1713744000, + "owned_by": "anthropic" + }, + { + "id": "claude-3-opus", + "object": "model", + "created": 1709251200, + "owned_by": "anthropic" + }, + { + "id": "claude-3.5-haiku", + "object": "model", + "created": 1711929600, + "owned_by": "anthropic" + }, + { + "id": "claude-3.5-sonnet", + "object": "model", + "created": 1711929600, + "owned_by": "anthropic" + }, + { + "id": "cursor-small", + "object": "model", + "created": 1712534400, + "owned_by": "cursor" + }, + { + "id": "gpt-3.5-turbo", + "object": "model", + "created": 1677649200, + "owned_by": "openai" + }, + { + "id": "gpt-4", + "object": "model", + "created": 1687392000, + "owned_by": "openai" + }, + { + "id": "gpt-4-turbo-2024-04-09", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "gpt-4o", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "gpt-4o-mini", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "o1-mini", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + }, + { + "id": "o1-preview", + "object": "model", + "created": 1712620800, + "owned_by": "openai" + } + ] + } + +if __name__ == "__main__": + import uvicorn + port = int(os.getenv("PORT", "3001")) + uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True, timeout_keep_alive=30) diff --git a/rs-capi/src/hex_utils.rs b/rs-capi/src/hex_utils.rs new file mode 100644 index 0000000..9c24f4e --- /dev/null +++ b/rs-capi/src/hex_utils.rs @@ -0,0 +1,111 @@ +pub fn string_to_hex(text: &str, model_name: &str) -> Vec { + let text_bytes = text.as_bytes(); + let text_length = text_bytes.len(); + + // 固定常量 + const FIXED_HEADER: usize = 2; + const SEPARATOR: usize = 1; + + let model_name_bytes = model_name.as_bytes(); + let fixed_suffix_length = 0xA3 + model_name_bytes.len(); + + // 计算第一个长度字段 + let (text_length_field1, text_length_field_size1) = if text_length < 128 { + (format!("{:02x}", text_length), 1) + } else { + let low_byte1 = (text_length & 0x7F) | 0x80; + let high_byte1 = (text_length >> 7) & 0xFF; + (format!("{:02x}{:02x}", low_byte1, high_byte1), 2) + }; + + // 计算基础长度字段 + let base_length = text_length + 0x2A; + let (text_length_field, text_length_field_size) = if base_length < 128 { + (format!("{:02x}", base_length), 1) + } else { + let low_byte = (base_length & 0x7F) | 0x80; + let high_byte = (base_length >> 7) & 0xFF; + (format!("{:02x}{:02x}", low_byte, high_byte), 2) + }; + + // 计算总消息长度 + let message_total_length = FIXED_HEADER + text_length_field_size + SEPARATOR + + text_length_field_size1 + text_length + fixed_suffix_length; + + // 构造十六进制字符串 + let model_name_length_hex = format!("{:02X}", model_name_bytes.len()); + let model_name_hex = hex::encode_upper(model_name_bytes); + + let hex_string = format!( + "{:010x}\ + 12{}\ + 0A{}\ + {}\ + 10016A2432343163636435662D393162612D343131382D393239612D3936626330313631626432612\ + 2002A132F643A2F6964656150726F2F656475626F73733A1E0A\ + {}{}\ + 22004A\ + 2461383761396133342D323164642D343863372D623434662D616636633365636536663765\ + 680070007A2436393337376535612D386332642D343835342D623564392D653062623232336163303061\ + 800101B00100C00100E00100E80100", + message_total_length, + text_length_field, + text_length_field1, + hex::encode_upper(text_bytes), + model_name_length_hex, + model_name_hex + ).to_uppercase(); + + // 将十六进制字符串转换为字节数组 + hex::decode(hex_string).unwrap_or_default() +} + +pub fn chunk_to_utf8_string(chunk: &[u8]) -> String { + if chunk.len() < 2 { + return String::new(); + } + + if chunk[0] == 0x01 || chunk[0] == 0x02 || (chunk[0] == 0x60 && chunk[1] == 0x0C) { + return String::new(); + } + + // 尝试找到0x0A并从其后开始处理 + let chunk = match chunk.iter().position(|&x| x == 0x0A) { + Some(pos) => &chunk[pos + 1..], + None => chunk + }; + + let mut filtered_chunk = Vec::new(); + let mut i = 0; + + while i < chunk.len() { + // 检查是否有连续的0x00 + if i + 4 <= chunk.len() && chunk[i..i+4].iter().all(|&x| x == 0x00) { + i += 4; + while i < chunk.len() && chunk[i] <= 0x0F { + i += 1; + } + continue; + } + + if chunk[i] == 0x0C { + i += 1; + while i < chunk.len() && chunk[i] == 0x0A { + i += 1; + } + } else { + filtered_chunk.push(chunk[i]); + i += 1; + } + } + + // 过滤掉特定字节 + filtered_chunk.retain(|&b| b != 0x00 && b != 0x0C); + + if filtered_chunk.is_empty() { + return String::new(); + } + + // 转换为UTF-8字符串 + String::from_utf8_lossy(&filtered_chunk).trim().to_string() +} \ No newline at end of file diff --git a/rs-capi/src/main.rs b/rs-capi/src/main.rs new file mode 100644 index 0000000..046fd66 --- /dev/null +++ b/rs-capi/src/main.rs @@ -0,0 +1,352 @@ +use axum::{ + http::{HeaderMap, StatusCode}, + response::{ + sse::{Event, Sse}, + IntoResponse, Response, + }, + routing::post, + Json, Router, +}; +use tower_http::trace::TraceLayer; + +use bytes::Bytes; +use futures::{ + channel::mpsc, + stream::{Stream, StreamExt}, + SinkExt, +}; +// use http::HeaderName as HttpHeaderName; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use std::{convert::Infallible, time::Duration}; +use tower_http::cors::{Any, CorsLayer}; +use uuid::Uuid; + +mod hex_utils; +use hex_utils::{chunk_to_utf8_string, string_to_hex}; + +// 定义请求模型 +#[derive(Debug, Deserialize)] +struct Message { + role: String, + content: String, +} + +#[derive(Debug, Deserialize)] +struct ChatRequest { + model: String, + messages: Vec, + #[serde(default)] + stream: bool, +} + +// 定义响应模型 +#[derive(Debug, Serialize)] +struct ChatResponse { + id: String, + object: String, + created: i64, + model: String, + choices: Vec, + usage: Usage, +} + +#[derive(Debug, Serialize)] +struct Choice { + index: i32, + message: ResponseMessage, + finish_reason: String, +} + +#[derive(Debug, Serialize)] +struct ResponseMessage { + role: String, + content: String, +} + +#[derive(Debug, Serialize)] +struct Usage { + prompt_tokens: i32, + completion_tokens: i32, + total_tokens: i32, +} + +#[derive(Debug, Serialize)] +struct StreamResponse { + id: String, + object: String, + created: i64, + choices: Vec, +} + +#[derive(Debug, Serialize)] +struct StreamChoice { + index: i32, + delta: Delta, +} + +#[derive(Debug, Serialize)] +struct Delta { + content: String, +} + +async fn process_stream( + chunks: Vec, +) -> impl Stream> + Send { + let (mut tx, rx) = mpsc::channel(100); + let response_id = format!("chatcmpl-{}", Uuid::new_v4()); + + tokio::spawn(async move { + for chunk in chunks { + let text = chunk_to_utf8_string(&chunk); + if !text.is_empty() { + let text = text.trim(); + let text = if let Some(idx) = text.find("<|END_USER|>") { + text[idx + "<|END_USER|>".len()..].trim() + } else { + text + }; + + let text = if !text.is_empty() && text.chars().next().unwrap().is_alphabetic() { + text[1..].trim() + } else { + text + }; + + let re = Regex::new(r"[\x00-\x1F\x7F]").unwrap(); + let text = re.replace_all(text, ""); + + if !text.is_empty() { + let response = StreamResponse { + id: response_id.clone(), + object: "chat.completion.chunk".to_string(), + created: chrono::Utc::now().timestamp(), + choices: vec![StreamChoice { + index: 0, + delta: Delta { + content: text.to_string(), + }, + }], + }; + + let json_data = serde_json::to_string(&response).unwrap(); + if !json_data.is_empty() { + let _ = tx.send(Ok(Event::default().data(json_data))).await; + } + } + } + } + + let _ = tx.send(Ok(Event::default().data("[DONE]"))).await; + }); + + rx +} + +#[tokio::main] +async fn main() { + // 初始化日志 + tracing_subscriber::fmt::init(); + + // 创建CORS中间件 + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_headers(Any); + + // 创建路由 + let app = Router::new() + .route("/v1/chat/completions", post(chat_completions)) + .route("/models", post(models)) + .layer(cors) + .layer( + TraceLayer::new_for_http() + .make_span_with(|request: &axum::http::Request<_>| { + tracing::info_span!( + "http_request", + method = %request.method(), + uri = %request.uri(), + ) + }) + // .on_request(|_request: &axum::http::Request<_>, _span: &tracing::Span| { info!("started processing request"); }) + .on_response( + |response: &axum::http::Response<_>, + latency: std::time::Duration, + _span: &tracing::Span| { + tracing::info!( + status = %response.status(), + latency = ?latency, + ); + }, + ), + ); + + // 启动服务器 + let addr = "0.0.0.0:3002"; + println!("Server running on {}", addr); + + // 修改服务器启动代码 + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} + +// 处理聊天完成请求 +async fn chat_completions( + headers: HeaderMap, + Json(chat_request): Json, +) -> Result { + // 验证认证 + let auth_header = headers + .get("authorization") + .and_then(|h| h.to_str().ok()) + .ok_or(StatusCode::UNAUTHORIZED)?; + + if !auth_header.starts_with("Bearer ") { + return Err(StatusCode::UNAUTHORIZED); + } + + let mut auth_token = auth_header.replace("Bearer ", ""); + + // 验证o1模型不支持流式输出 + if chat_request.model.starts_with("o1-") && chat_request.stream { + return Err(StatusCode::BAD_REQUEST); + } + tracing::info!("chat_request: {:?}", chat_request); + + // 处理多个密钥 + if auth_token.contains(',') { + auth_token = auth_token.split(',').next().unwrap().trim().to_string(); + } + + if auth_token.contains("%3A%3A") { + auth_token = auth_token + .split("%3A%3A") + .nth(1) + .unwrap_or(&auth_token) + .to_string(); + } + + // 格式化消息 + let formatted_messages = chat_request + .messages + .iter() + .map(|msg| format!("{}:{}", msg.role, msg.content)) + .collect::>() + .join("\n"); + + // 生成请求数据 + let hex_data = string_to_hex(&formatted_messages, &chat_request.model); + // 准备请求头 + let request_id = Uuid::new_v4(); + let headers = reqwest::header::HeaderMap::from_iter([ + (reqwest::header::CONTENT_TYPE, "application/connect+proto"), + (reqwest::header::AUTHORIZATION, &format!("Bearer {}", auth_token)), + // 对于标准 HTTP 头部,使用预定义的常量 + (reqwest::header::HeaderName::from_str("Connect-Accept-Encoding").unwrap(), "gzip,br"), + (reqwest::header::HeaderName::from_str("Connect-Protocol-Version").unwrap(), "1"), + (reqwest::header::HeaderName::from_str("User-Agent").unwrap(), "connect-es/1.4.0"), + (reqwest::header::HeaderName::from_str("X-Amzn-Trace-Id").unwrap(), &format!("Root={}", Uuid::new_v4())), + (reqwest::header::HeaderName::from_str("X-Cursor-Checksum").unwrap(), "zo6Qjequ9b9734d1f13c3438ba25ea31ac93d9287248b9d30434934e9fcbfa6b3b22029e/7e4af391f67188693b722eff0090e8e6608bca8fa320ef20a0ccb5d7d62dfdef"), + (reqwest::header::HeaderName::from_str("X-Cursor-Client-Version").unwrap(), "0.42.3"), + (reqwest::header::HeaderName::from_str("X-Cursor-Timezone").unwrap(), "Asia/Shanghai"), + (reqwest::header::HeaderName::from_str("X-Ghost-Mode").unwrap(), "false"), + (reqwest::header::HeaderName::from_str("X-Request-Id").unwrap(), &request_id.to_string()), + (reqwest::header::HeaderName::from_str("Host").unwrap(), "api2.cursor.sh"), + ].iter().map(|(k, v)| ( + k.clone(), + reqwest::header::HeaderValue::from_str(v).unwrap() + ))); + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(300)) + .build() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let response = client + .post("https://api2.cursor.sh/aiserver.v1.AiService/StreamChat") + .headers(headers) + .body(hex_data) + .send() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + if chat_request.stream { + let mut chunks = Vec::new(); + let mut stream = response.bytes_stream(); + + while let Some(chunk) = stream.next().await { + match chunk { + Ok(chunk) => chunks.push(chunk), + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + } + } + + let stream = process_stream(chunks).await; + return Ok(Sse::new(stream).into_response()); + } + + // 非流式响应 + let mut text = String::new(); + let mut stream = response.bytes_stream(); + + while let Some(chunk) = stream.next().await { + match chunk { + Ok(chunk) => { + let res = chunk_to_utf8_string(&chunk); + if !res.is_empty() { + text.push_str(&res); + } + } + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + } + } + + // 清理响应文本 + let re = Regex::new(r"^.*<\|END_USER\|>").unwrap(); + text = re.replace(&text, "").to_string(); + + let re = Regex::new(r"^\n[a-zA-Z]?").unwrap(); + text = re.replace(&text, "").trim().to_string(); + + let re = Regex::new(r"[\x00-\x1F\x7F]").unwrap(); + text = re.replace_all(&text, "").to_string(); + + let response = ChatResponse { + id: format!("chatcmpl-{}", Uuid::new_v4()), + object: "chat.completion".to_string(), + created: chrono::Utc::now().timestamp(), + model: chat_request.model, + choices: vec![Choice { + index: 0, + message: ResponseMessage { + role: "assistant".to_string(), + content: text, + }, + finish_reason: "stop".to_string(), + }], + usage: Usage { + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + }, + }; + + Ok(Json(response).into_response()) +} + +// 处理模型列表请求 +async fn models() -> Json { + Json(serde_json::json!({ + "object": "list", + "data": [ + { + "id": "claude-3-5-sonnet-20241022", + "object": "model", + "created": 1713744000, + "owned_by": "anthropic" + }, + // ... 其他模型 + ] + })) +} diff --git a/types.go b/types.go deleted file mode 100644 index 2385e17..0000000 --- a/types.go +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 0077c24..0000000 --- a/utils.go +++ /dev/null @@ -1,83 +0,0 @@ -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/utils/hex.go b/utils/hex.go new file mode 100644 index 0000000..cf86355 --- /dev/null +++ b/utils/hex.go @@ -0,0 +1,158 @@ +package utils + +import ( + "bytes" + "encoding/hex" + "fmt" + "strings" + "unicode/utf8" +) + +func StringToHex(text string, modelName string) ([]byte, error) { + textBytes := []byte(text) + textLength := len(textBytes) + + const ( + FIXED_HEADER = 2 + SEPARATOR = 1 + ) + + modelNameBytes := []byte(modelName) + FIXED_SUFFIX_LENGTH := 0xA3 + len(modelNameBytes) + + // 计算第一个长度字段 + var textLengthField1 string + var textLengthFieldSize1 int + if textLength < 128 { + textLengthField1 = fmt.Sprintf("%02x", textLength) + textLengthFieldSize1 = 1 + } else { + lowByte1 := (textLength & 0x7F) | 0x80 + highByte1 := (textLength >> 7) & 0xFF + textLengthField1 = fmt.Sprintf("%02x%02x", lowByte1, highByte1) + textLengthFieldSize1 = 2 + } + + // 计算基础长度字段 + baseLength := textLength + 0x2A + var textLengthField string + var textLengthFieldSize int + if baseLength < 128 { + textLengthField = fmt.Sprintf("%02x", baseLength) + textLengthFieldSize = 1 + } else { + lowByte := (baseLength & 0x7F) | 0x80 + highByte := (baseLength >> 7) & 0xFF + textLengthField = fmt.Sprintf("%02x%02x", lowByte, highByte) + textLengthFieldSize = 2 + } + + // 计算总消息长度 + messageTotalLength := FIXED_HEADER + textLengthFieldSize + SEPARATOR + textLengthFieldSize1 + textLength + FIXED_SUFFIX_LENGTH + + modelNameHex := strings.ToUpper(hex.EncodeToString(modelNameBytes)) + modelNameLengthHex := fmt.Sprintf("%02X", len(modelNameBytes)) + + hexString := fmt.Sprintf( + "%010x"+ + "12"+ + "%s"+ + "0a"+ + "%s"+ + "%x"+ + "10016a2432343163636435662d393162612d343131382d393239612d3936626330313631626432612"+ + "2002a132f643a2f6964656150726f2f656475626f73733a1e0a"+ + "%s"+ + "%s"+ + "22004a"+ + "2461383761396133342d323164642d343863372d623434662d616636633365636536663765"+ + "680070007a2436393737376535612d386332642d343835342d623564392d653062623232336163303061"+ + "800101b00100c00100e00100e80100", + messageTotalLength, + textLengthField, + textLengthField1, + textBytes, + modelNameLengthHex, + modelNameHex, + ) + + hexString = strings.ToLower(hexString) + + return hex.DecodeString(hexString) +} + +func ChunkToUTF8String(chunk []byte) string { + // 基础检查 + if len(chunk) < 2 { + return "" + } + + if chunk[0] == 0x01 || chunk[0] == 0x02 || (chunk[0] == 0x60 && chunk[1] == 0x0C) { + return "" + } + + // 修改调试输出格式 + // fmt.Printf("chunk length: %d hex: %x\n", len(chunk), chunk) + fmt.Printf("chunk length: %d\n", len(chunk), ) + + // 去掉0x0A之前的所有字节 + if idx := bytes.IndexByte(chunk, 0x0A); idx != -1 { + chunk = chunk[idx+1:] + } + + // 修改过滤逻辑,将过滤步骤分开 + filteredChunk := make([]byte, 0, len(chunk)) + i := 0 + for i < len(chunk) { + // 检查连续的0x00 + if i+4 <= len(chunk) && allZeros(chunk[i:i+4]) { + i += 4 + for i < len(chunk) && chunk[i] <= 0x0F { + i++ + } + continue + } + + if chunk[i] == 0x0C { + i++ + for i < len(chunk) && chunk[i] == 0x0A { + i++ + } + } else { + filteredChunk = append(filteredChunk, chunk[i]) + i++ + } + } + + // 最后统一过滤特定字节 + finalFiltered := make([]byte, 0, len(filteredChunk)) + for _, b := range filteredChunk { + if b != 0x00 && b != 0x0C { + finalFiltered = append(finalFiltered, b) + } + } + + if len(finalFiltered) == 0 { + return "" + } + + // 添加错误处理 + result := strings.TrimSpace(string(finalFiltered)) + if !utf8.Valid(finalFiltered) { + fmt.Printf("Error: Invalid UTF-8 sequence\n") + return "" + } + + fmt.Printf("decoded result: %s\n", result) + return result +} + +// 辅助函数检查连续的零字节 +func allZeros(data []byte) bool { + for _, b := range data { + if b != 0x00 { + return false + } + } + return true +} \ No newline at end of file