ricebug commited on
Commit
35ada9e
·
verified ·
1 Parent(s): 9c566b6

Upload 27 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 构建阶段
2
+ FROM golang:1.24-alpine AS builder
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 安装必要的包
8
+ RUN apk add --no-cache git ca-certificates
9
+
10
+ # 复制go mod文件
11
+ COPY go.mod go.sum ./
12
+
13
+ # 下载依赖
14
+ RUN go mod download
15
+
16
+ # 复制源码
17
+ COPY . .
18
+
19
+ # 构建应用
20
+ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o cursor2api-go .
21
+
22
+ # 运行阶段
23
+ FROM alpine:latest
24
+
25
+ # 安装 ca-certificates 和 nodejs(用于 JavaScript 执行)
26
+ RUN apk --no-cache add ca-certificates nodejs npm
27
+
28
+ # 创建非 root 用户
29
+ RUN adduser -D -g '' appuser
30
+
31
+ WORKDIR /root/
32
+
33
+ # 从构建阶段复制二进制文件
34
+ COPY --from=builder /app/cursor2api-go .
35
+
36
+ # 复制静态文件和 JS 代码(需要用于 JavaScript 执行)
37
+ COPY --from=builder /app/static ./static
38
+ COPY --from=builder /app/jscode ./jscode
39
+
40
+ # 更改所有者
41
+ RUN chown -R appuser:appuser /root/
42
+
43
+ # 切换到非root用户
44
+ USER appuser
45
+
46
+ # 暴露端口
47
+ EXPOSE 8002
48
+
49
+ # 健康检查
50
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
51
+ CMD node -e "require('http').get('http://localhost:8002/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))" || exit 1
52
+
53
+ # 启动应用
54
+ CMD ["./cursor2api-go"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 libaxuan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
TROUBLESHOOTING.md ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 故障排除指南
2
+
3
+ ## 403 Access Denied 错误
4
+
5
+ ### 问题描述
6
+ 在使用一段时间后,服务突然开始返回 `403 Access Denied` 错误:
7
+ ```
8
+ ERRO[0131] Cursor API returned non-OK status status_code=403
9
+ ERRO[0131] Failed to create chat completion error="{\"error\":\"Access denied\"}"
10
+ ```
11
+
12
+ ### 原因分析
13
+ 1. **Token 过期**: `x-is-human` token 缓存时间过长,导致 token 失效
14
+ 2. **频率限制**: 短时间内发送过多请求触发了 Cursor API 的速率限制
15
+ 3. **重复 Token**: 使用相同的 token 进行多次请求被识别为异常行为
16
+
17
+ ### 解决方案
18
+
19
+ #### 1. 已实施的自动修复
20
+ 最新版本已经包含以下改进:
21
+
22
+ - **动态浏览器指纹**: 每次请求使用真实且随机的浏览器指纹信息
23
+ - 根据操作系统自动选择合适的平台配置 (Windows/macOS/Linux)
24
+ - 随机 Chrome 版本 (120-130)
25
+ - 随机语言设置和 Referer
26
+ - 真实的 User-Agent 和 sec-ch-ua headers
27
+ - **缩短缓存时间**: 将 `x-is-human` token 缓存时间从 30 分钟缩短到 1 分钟
28
+ - **自动重试机制**: 遇到 403 错误时自动清除缓存并重试(最多 2 次)
29
+ - **指纹刷新**: 403 错误时自动刷新浏览器指纹配置
30
+ - **错误恢复**: 失败时自动清除缓存,确保下次请求使用新 token
31
+ - **指数退避**: 重试时使用递增的等待时间
32
+
33
+ #### 2. 手动解决步骤
34
+ 如果问题持续存在:
35
+
36
+ 1. **重启服务**:
37
+ ```bash
38
+ # 停止当前服务 (Ctrl+C)
39
+ # 重新启动
40
+ ./cursor2api-go
41
+ ```
42
+
43
+ 2. **检查日志**:
44
+ 查看是否有以下日志:
45
+ - `Received 403 Access Denied, clearing token cache and retrying...` - 自动重试
46
+ - `Failed to fetch x-is-human token` - Token 获取失败
47
+ - `Fetched x-is-human token` - Token 获取成功
48
+
49
+ 3. **等待冷却期**:
50
+ 如果频繁遇到 403 错误,建议等待 5-10 分钟后再使用
51
+
52
+ 4. **检查网络**:
53
+ 确保能够访问 `https://cursor.com`
54
+
55
+ #### 3. 预防措施
56
+
57
+ 1. **控制请求频率**: 避免在短时间内发送大量请求
58
+ 2. **监控日志**: 注意 `x-is-human token` 的获取频率
59
+ 3. **合理配置超时**: 在 `.env` 文件中设置合理的超时时间
60
+
61
+ ### 配置建议
62
+
63
+ 在 `.env` 文件中:
64
+ ```bash
65
+ TIMEOUT=120 # 增加超时时间,避免频繁重试
66
+ MAX_INPUT_LENGTH=100000 # 限制输入长度,减少请求大小
67
+ ```
68
+
69
+ ### 调试模式
70
+
71
+ 如果需要查看详细的调试信息,可以启用调试模式:
72
+ ```bash
73
+ # 方式 1: 修改 .env 文件
74
+ DEBUG=true
75
+
76
+ # 方式 2: 使用环境变量
77
+ DEBUG=true ./cursor2api-go
78
+ ```
79
+
80
+ 这将显示:
81
+ - 每次请求的 `x-is-human` token (前 50 字符)
82
+ - 请求的 payload 大小
83
+ - 重试次数
84
+ - 详细的错误信息
85
+
86
+ ## 其他常见问题
87
+
88
+ ### Cloudflare 403 错误
89
+ 如果看到 `Cloudflare 403` 错误,说明请求被 Cloudflare 防火墙拦截。这通常是因为:
90
+ - IP 被标记为可疑
91
+ - User-Agent 不匹配
92
+ - 缺少必要的浏览器指纹
93
+
94
+ **解决方案**: 检查 `.env` 文件中的浏览器指纹配置(`USER_AGENT`、`UNMASKED_VENDOR_WEBGL`、`UNMASKED_RENDERER_WEBGL`)是否正确。
95
+
96
+ ### 连接超时
97
+ 如果频繁出现连接超时:
98
+ 1. 检查网络连接
99
+ 2. 增加 `.env` 文件中的 `TIMEOUT` 配置值
100
+ 3. 检查防火墙设置
101
+
102
+ ### Token 获取失败
103
+ 如果无法获取 `x-is-human` token:
104
+ 1. 检查 `.env` 文件中的 `SCRIPT_URL` 配置是否正确
105
+ 2. 确保 `jscode/main.js` 和 `jscode/env.js` 文件存在
106
+ 3. 检查 Node.js 环境是否正常安装(Node.js 18+)
107
+
108
+ ## 联系支持
109
+
110
+ 如果问题仍未解决,请提供以下信息:
111
+ 1. 完整的错误日志
112
+ 2. `.env` 文件配置(隐藏敏感信息如 `API_KEY`)
113
+ 3. 使用的 Go 版本和 Node.js 版本
114
+ 4. 操作系统信息
config/config.go ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package config
22
+
23
+ import (
24
+ "encoding/json"
25
+ "fmt"
26
+ "os"
27
+ "strconv"
28
+ "strings"
29
+
30
+ "github.com/joho/godotenv"
31
+ "github.com/sirupsen/logrus"
32
+ )
33
+
34
+ // Config 应用程序配置结构
35
+ type Config struct {
36
+ // 服务器配置
37
+ Port int `json:"port"`
38
+ Debug bool `json:"debug"`
39
+
40
+ // API配置
41
+ APIKey string `json:"api_key"`
42
+ Models string `json:"models"`
43
+ SystemPromptInject string `json:"system_prompt_inject"`
44
+ Timeout int `json:"timeout"`
45
+ MaxInputLength int `json:"max_input_length"`
46
+
47
+ // Cursor相关配置
48
+ ScriptURL string `json:"script_url"`
49
+ FP FP `json:"fp"`
50
+ }
51
+
52
+ // FP 指纹配置结构
53
+ type FP struct {
54
+ UserAgent string `json:"userAgent"`
55
+ UNMASKED_VENDOR_WEBGL string `json:"unmaskedVendorWebgl"`
56
+ UNMASKED_RENDERER_WEBGL string `json:"unmaskedRendererWebgl"`
57
+ }
58
+
59
+ // LoadConfig 加载配置
60
+ func LoadConfig() (*Config, error) {
61
+ // 尝试加载.env文件
62
+ if err := godotenv.Load(); err != nil {
63
+ logrus.Debug("No .env file found, using environment variables")
64
+ }
65
+
66
+ config := &Config{
67
+ // 设置默认值
68
+ Port: getEnvAsInt("PORT", 8002),
69
+ Debug: getEnvAsBool("DEBUG", false),
70
+ APIKey: getEnv("API_KEY", "0000"),
71
+ Models: getEnv("MODELS", "gpt-4o,claude-3.5-sonnet"),
72
+ SystemPromptInject: getEnv("SYSTEM_PROMPT_INJECT", ""),
73
+ Timeout: getEnvAsInt("TIMEOUT", 60),
74
+ MaxInputLength: getEnvAsInt("MAX_INPUT_LENGTH", 200000),
75
+ ScriptURL: getEnv("SCRIPT_URL", "https://cursor.com/_next/static/chunks/pages/_app.js"),
76
+ FP: FP{
77
+ UserAgent: getEnv("USER_AGENT", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"),
78
+ UNMASKED_VENDOR_WEBGL: getEnv("UNMASKED_VENDOR_WEBGL", "Google Inc. (Intel)"),
79
+ UNMASKED_RENDERER_WEBGL: getEnv("UNMASKED_RENDERER_WEBGL", "ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11)"),
80
+ },
81
+ }
82
+
83
+ // 验证必要的配置
84
+ if err := config.validate(); err != nil {
85
+ return nil, fmt.Errorf("config validation failed: %w", err)
86
+ }
87
+
88
+ return config, nil
89
+ }
90
+
91
+ // validate 验证配置
92
+ func (c *Config) validate() error {
93
+ if c.Port <= 0 || c.Port > 65535 {
94
+ return fmt.Errorf("invalid port: %d", c.Port)
95
+ }
96
+
97
+ if c.APIKey == "" {
98
+ return fmt.Errorf("API_KEY is required")
99
+ }
100
+
101
+ if c.Timeout <= 0 {
102
+ return fmt.Errorf("timeout must be positive")
103
+ }
104
+
105
+ if c.MaxInputLength <= 0 {
106
+ return fmt.Errorf("max input length must be positive")
107
+ }
108
+
109
+ return nil
110
+ }
111
+
112
+ // GetModels 获取模型列表
113
+ func (c *Config) GetModels() []string {
114
+ models := strings.Split(c.Models, ",")
115
+ result := make([]string, 0, len(models))
116
+ for _, model := range models {
117
+ if trimmed := strings.TrimSpace(model); trimmed != "" {
118
+ result = append(result, trimmed)
119
+ }
120
+ }
121
+ return result
122
+ }
123
+
124
+ // IsValidModel 检查模型是否有效
125
+ func (c *Config) IsValidModel(model string) bool {
126
+ validModels := c.GetModels()
127
+ for _, validModel := range validModels {
128
+ if validModel == model {
129
+ return true
130
+ }
131
+ }
132
+ return false
133
+ }
134
+
135
+ // ToJSON 将配置序列化为JSON(用于调试)
136
+ func (c *Config) ToJSON() string {
137
+ // 创建一个副本,隐藏敏感信息
138
+ safeCfg := *c
139
+ safeCfg.APIKey = "***"
140
+
141
+ data, err := json.MarshalIndent(safeCfg, "", " ")
142
+ if err != nil {
143
+ return fmt.Sprintf("Error marshaling config: %v", err)
144
+ }
145
+ return string(data)
146
+ }
147
+
148
+ // 辅助函数
149
+
150
+ // getEnv 获取环境变量,如果不存在则返回默认值
151
+ func getEnv(key, defaultValue string) string {
152
+ if value := os.Getenv(key); value != "" {
153
+ return value
154
+ }
155
+ return defaultValue
156
+ }
157
+
158
+ // getEnvAsInt 获取环境变量并转换为int
159
+ func getEnvAsInt(key string, defaultValue int) int {
160
+ valueStr := os.Getenv(key)
161
+ if valueStr == "" {
162
+ return defaultValue
163
+ }
164
+
165
+ value, err := strconv.Atoi(valueStr)
166
+ if err != nil {
167
+ logrus.Warnf("Invalid integer value for %s: %s, using default: %d", key, valueStr, defaultValue)
168
+ return defaultValue
169
+ }
170
+
171
+ return value
172
+ }
173
+
174
+ // getEnvAsBool 获取环境变量并转换为bool
175
+ func getEnvAsBool(key string, defaultValue bool) bool {
176
+ valueStr := os.Getenv(key)
177
+ if valueStr == "" {
178
+ return defaultValue
179
+ }
180
+
181
+ value, err := strconv.ParseBool(valueStr)
182
+ if err != nil {
183
+ logrus.Warnf("Invalid boolean value for %s: %s, using default: %t", key, valueStr, defaultValue)
184
+ return defaultValue
185
+ }
186
+
187
+ return value
188
+ }
config/config_test.go ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package config
22
+
23
+ import (
24
+ "os"
25
+ "testing"
26
+ )
27
+
28
+ func TestLoadConfig(t *testing.T) {
29
+ // Create a temporary .env file for testing
30
+ envContent := `PORT=9000
31
+ DEBUG=true
32
+ API_KEY=test-key
33
+ MODELS=gpt-4o,claude-3
34
+ SYSTEM_PROMPT_INJECT=Test prompt
35
+ TIMEOUT=60
36
+ MAX_INPUT_LENGTH=10000
37
+ USER_AGENT=Test Agent
38
+ SCRIPT_URL=https://test.com/script.js`
39
+
40
+ // Write to temporary .env file
41
+ err := os.WriteFile(".env", []byte(envContent), 0644)
42
+ if err != nil {
43
+ t.Fatalf("Failed to create test .env file: %v", err)
44
+ }
45
+ defer os.Remove(".env") // Clean up
46
+
47
+ config, err := LoadConfig()
48
+ if err != nil {
49
+ t.Fatalf("LoadConfig() error = %v", err)
50
+ }
51
+
52
+ // Test loaded values
53
+ if config.Port != 9000 {
54
+ t.Errorf("Port = %v, want 9000", config.Port)
55
+ }
56
+ if !config.Debug {
57
+ t.Errorf("Debug = %v, want true", config.Debug)
58
+ }
59
+ if config.APIKey != "test-key" {
60
+ t.Errorf("APIKey = %v, want test-key", config.APIKey)
61
+ }
62
+ if config.SystemPromptInject != "Test prompt" {
63
+ t.Errorf("SystemPromptInject = %v, want Test prompt", config.SystemPromptInject)
64
+ }
65
+ if config.Timeout != 60 {
66
+ t.Errorf("Timeout = %v, want 60", config.Timeout)
67
+ }
68
+ if config.MaxInputLength != 10000 {
69
+ t.Errorf("MaxInputLength = %v, want 10000", config.MaxInputLength)
70
+ }
71
+ if config.FP.UserAgent != "Test Agent" {
72
+ t.Errorf("UserAgent = %v, want Test Agent", config.FP.UserAgent)
73
+ }
74
+ if config.ScriptURL != "https://test.com/script.js" {
75
+ t.Errorf("ScriptURL = %v, want https://test.com/script.js", config.ScriptURL)
76
+ }
77
+ }
78
+
79
+ func TestGetModels(t *testing.T) {
80
+ config := &Config{
81
+ Models: "gpt-4o, claude-3 , gpt-3.5",
82
+ }
83
+
84
+ models := config.GetModels()
85
+ expected := []string{"gpt-4o", "claude-3", "gpt-3.5"}
86
+
87
+ if len(models) != len(expected) {
88
+ t.Errorf("GetModels() length = %v, want %v", len(models), len(expected))
89
+ }
90
+
91
+ for i, model := range models {
92
+ if model != expected[i] {
93
+ t.Errorf("GetModels()[%d] = %v, want %v", i, model, expected[i])
94
+ }
95
+ }
96
+ }
97
+
98
+ func TestIsValidModel(t *testing.T) {
99
+ config := &Config{
100
+ Models: "gpt-4o,claude-3,gpt-3.5",
101
+ }
102
+
103
+ tests := []struct {
104
+ name string
105
+ model string
106
+ expected bool
107
+ }{
108
+ {"valid model gpt-4o", "gpt-4o", true},
109
+ {"valid model claude-3", "claude-3", true},
110
+ {"invalid model gpt-5", "gpt-5", false},
111
+ {"empty model", "", false},
112
+ }
113
+
114
+ for _, tt := range tests {
115
+ t.Run(tt.name, func(t *testing.T) {
116
+ result := config.IsValidModel(tt.model)
117
+ if result != tt.expected {
118
+ t.Errorf("IsValidModel(%q) = %v, want %v", tt.model, result, tt.expected)
119
+ }
120
+ })
121
+ }
122
+ }
123
+
124
+ func TestValidate(t *testing.T) {
125
+ tests := []struct {
126
+ name string
127
+ config *Config
128
+ wantErr bool
129
+ }{
130
+ {
131
+ name: "valid config",
132
+ config: &Config{
133
+ Port: 8000,
134
+ APIKey: "test-key",
135
+ Timeout: 30,
136
+ MaxInputLength: 1000,
137
+ },
138
+ wantErr: false,
139
+ },
140
+ {
141
+ name: "invalid port - too low",
142
+ config: &Config{
143
+ Port: 0,
144
+ APIKey: "test-key",
145
+ Timeout: 30,
146
+ MaxInputLength: 1000,
147
+ },
148
+ wantErr: true,
149
+ },
150
+ {
151
+ name: "invalid port - too high",
152
+ config: &Config{
153
+ Port: 70000,
154
+ APIKey: "test-key",
155
+ Timeout: 30,
156
+ MaxInputLength: 1000,
157
+ },
158
+ wantErr: true,
159
+ },
160
+ {
161
+ name: "missing API key",
162
+ config: &Config{
163
+ Port: 8000,
164
+ APIKey: "",
165
+ Timeout: 30,
166
+ MaxInputLength: 1000,
167
+ },
168
+ wantErr: true,
169
+ },
170
+ {
171
+ name: "invalid timeout",
172
+ config: &Config{
173
+ Port: 8000,
174
+ APIKey: "test-key",
175
+ Timeout: 0,
176
+ MaxInputLength: 1000,
177
+ },
178
+ wantErr: true,
179
+ },
180
+ {
181
+ name: "invalid max input length",
182
+ config: &Config{
183
+ Port: 8000,
184
+ APIKey: "test-key",
185
+ Timeout: 30,
186
+ MaxInputLength: 0,
187
+ },
188
+ wantErr: true,
189
+ },
190
+ }
191
+
192
+ for _, tt := range tests {
193
+ t.Run(tt.name, func(t *testing.T) {
194
+ err := tt.config.validate()
195
+ if (err != nil) != tt.wantErr {
196
+ t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
197
+ }
198
+ })
199
+ }
200
+ }
docker-compose.yml ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ cursor2api:
5
+ build: .
6
+ container_name: cursor2api-go
7
+ restart: unless-stopped
8
+ ports:
9
+ - "8002:8002"
10
+ environment:
11
+ # 服务器配置
12
+ - PORT=8002
13
+ - DEBUG=false
14
+
15
+ # API 配置(⚠️ 生产环境请修改默认密钥)
16
+ - API_KEY=0000
17
+ - MODELS=claude-sonnet-4.6
18
+ - SYSTEM_PROMPT_INJECT=
19
+
20
+ # 请求配置
21
+ - TIMEOUT=60
22
+ - MAX_INPUT_LENGTH=200000
23
+
24
+ # 浏览器指纹配置
25
+ - USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
26
+ - UNMASKED_VENDOR_WEBGL=Google Inc. (Intel)
27
+ - UNMASKED_RENDERER_WEBGL=ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11)
28
+
29
+ # Cursor 配置
30
+ - SCRIPT_URL=https://cursor.com/_next/static/chunks/pages/_app.js
31
+ healthcheck:
32
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8002/health"]
33
+ interval: 30s
34
+ timeout: 10s
35
+ retries: 3
36
+ start_period: 5s
37
+ networks:
38
+ - cursor2api-network
39
+
40
+ networks:
41
+ cursor2api-network:
42
+ driver: bridge
docs/DYNAMIC_HEADERS.md ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 动态 Header 改进说明
2
+
3
+ ## 问题背景
4
+
5
+ 之前的实现中,HTTP headers 是硬编码的:
6
+ - `sec-ch-ua-platform` 固定为 `"macOS"` 或 `"Windows"`
7
+ - `sec-ch-ua` 固定为特定的 Chrome 版本
8
+ - `Referer` 和 `accept-language` 固定不变
9
+
10
+ 这种硬编码的方式容易被 Cursor API 识别为异常请求,导致 403 错误。
11
+
12
+ ## 改进方案
13
+
14
+ ### 1. 动态浏览器指纹生成器 (`utils/headers.go`)
15
+
16
+ 创建了 `HeaderGenerator` 类,实现以下功能:
17
+
18
+ #### 智能平台选择
19
+ - 根据当前操作系统自动选择合适的浏览器配置
20
+ - macOS: 支持 Intel (x86) 和 Apple Silicon (arm) 架构
21
+ - Windows: 支持多个版本 (10.0, 11.0, 15.0)
22
+ - Linux: 标准 x86_64 配置
23
+
24
+ #### 随机化配置
25
+ - **Chrome 版本**: 从 120-130 随机选择
26
+ - **语言设置**: 支持 en-US, zh-CN, en-GB, ja-JP
27
+ - **Referer**: 随机选择不同的 Cursor 页面
28
+ - **User-Agent**: 根据平台和版本动态生成
29
+
30
+ #### 真实的浏览器指纹
31
+ 生成的 headers 包含完整的浏览器指纹信息:
32
+ ```json
33
+ {
34
+ "sec-ch-ua-platform": "macOS",
35
+ "sec-ch-ua-platform-version": "14.0.0",
36
+ "sec-ch-ua-arch": "arm",
37
+ "sec-ch-ua-bitness": "64",
38
+ "sec-ch-ua": "\"Google Chrome\";v=\"126\", \"Chromium\";v=\"126\", \"Not(A:Brand\";v=\"24\"",
39
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36..."
40
+ }
41
+ ```
42
+
43
+ ### 2. 自动刷新机制
44
+
45
+ 当遇到 403 错误时:
46
+ 1. 自动刷新浏览器指纹配置
47
+ 2. 清除 x-is-human token 缓存
48
+ 3. 使用新的配置重试请求
49
+
50
+ ### 3. 代码改进
51
+
52
+ #### 服务初始化
53
+ ```go
54
+ type CursorService struct {
55
+ // ... 其他字段
56
+ headerGenerator *utils.HeaderGenerator
57
+ }
58
+
59
+ func NewCursorService(cfg *config.Config) *CursorService {
60
+ return &CursorService{
61
+ // ... 其他初始化
62
+ headerGenerator: utils.NewHeaderGenerator(),
63
+ }
64
+ }
65
+ ```
66
+
67
+ #### Headers 生成
68
+ ```go
69
+ // 之前:硬编码
70
+ func (s *CursorService) chatHeaders(xIsHuman string) map[string]string {
71
+ return map[string]string{
72
+ "sec-ch-ua-platform": `"macOS"`, // 固定值
73
+ "sec-ch-ua": `"Google Chrome";v="143"...`, // 固定版本
74
+ // ...
75
+ }
76
+ }
77
+
78
+ // 现在:动态生成
79
+ func (s *CursorService) chatHeaders(xIsHuman string) map[string]string {
80
+ return s.headerGenerator.GetChatHeaders(xIsHuman)
81
+ }
82
+ ```
83
+
84
+ #### 403 错误处理
85
+ ```go
86
+ if resp.StatusCode == http.StatusForbidden && attempt < maxRetries {
87
+ logrus.Warn("Received 403, refreshing browser fingerprint...")
88
+
89
+ // 刷新浏览器指纹
90
+ s.headerGenerator.Refresh()
91
+
92
+ // 清除 token 缓存
93
+ s.scriptMutex.Lock()
94
+ s.scriptCache = ""
95
+ s.scriptCacheTime = time.Time{}
96
+ s.scriptMutex.Unlock()
97
+
98
+ // 重试
99
+ continue
100
+ }
101
+ ```
102
+
103
+ ## 优势
104
+
105
+ ### 1. 更难被检测
106
+ - 每次请求的指纹信息都可能不同
107
+ - 模拟真实用户的多样性
108
+ - 避免固定模式被识别
109
+
110
+ ### 2. 自动适应
111
+ - 根据运行环境自动选择合适的配置
112
+ - macOS 上运行自动使用 macOS 配置
113
+ - Windows 上运行自动使用 Windows 配置
114
+
115
+ ### 3. 更好的容错性
116
+ - 遇到 403 错误自动切换配置
117
+ - 增加请求成功率
118
+ - 减少人工干预
119
+
120
+ ### 4. 易于维护
121
+ - 集中管理浏览器配置
122
+ - 易于添加新的平台或版本
123
+ - 代码更简洁清晰
124
+
125
+ ## 测试结果
126
+
127
+ 运行测试程序可以看到:
128
+ ```
129
+ 浏览器配置:
130
+ 平台: macOS
131
+ 平台版本: 14.0.0
132
+ 架构: arm
133
+ 位数: 64
134
+ Chrome 版本: 126
135
+ User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...
136
+
137
+ 生成 5 个随机配置:
138
+ 1. macOS | Chrome 130 | arm
139
+ 2. macOS | Chrome 125 | arm
140
+ 3. macOS | Chrome 130 | x86
141
+ 4. macOS | Chrome 128 | arm
142
+ 5. macOS | Chrome 122 | arm
143
+ ```
144
+
145
+ 每次生成的配置都不同,增加了多样性。
146
+
147
+ ## 使用方法
148
+
149
+ 无需任何配置,直接使用即可:
150
+
151
+ ```bash
152
+ # 重新编译
153
+ go build -o cursor2api-go
154
+
155
+ # 运行服务
156
+ ./cursor2api-go
157
+ ```
158
+
159
+ 服务会自动:
160
+ - 根据操作系统选择合适的浏览器配置
161
+ - 为每个请求生成动态 headers
162
+ - 遇到 403 错误时自动刷新配置并重试
163
+
164
+ ## 日志示例
165
+
166
+ 启用调试模式后可以看到:
167
+ ```
168
+ DEBU Sending request to Cursor API attempt=1 model=claude-4.5-sonnet
169
+ WARN Received 403 Access Denied, refreshing browser fingerprint...
170
+ DEBU Refreshed browser fingerprint platform=macOS chrome_version=124
171
+ DEBU Sending request to Cursor API attempt=2 model=claude-4.5-sonnet
172
+ ```
173
+
174
+ ## 未来改进
175
+
176
+ 可以考虑的进一步优化:
177
+ 1. 添加更多浏览器类型 (Firefox, Safari)
178
+ 2. 支持移动设备指纹
179
+ 3. 根据成功率动态调整配置策略
180
+ 4. 添加指纹轮换策略 (定期刷新)
docs/STARTUP_OPTIMIZATION.md ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 启动日志优化说明
2
+
3
+ ## 优化前 vs 优化后
4
+
5
+ ### 优化前(调试模式)
6
+ 启动时会显示大量 GIN 框架的调试信息:
7
+ ```
8
+ [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
9
+ - using env: export GIN_MODE=release
10
+ - using code: gin.SetMode(gin.ReleaseMode)
11
+
12
+ [GIN-debug] GET /health --> main.setupRoutes.func1 (5 handlers)
13
+ [GIN-debug] GET / --> cursor2api-go/handlers.(*Handler).ServeDocs-fm (5 handlers)
14
+ [GIN-debug] GET /v1/models --> cursor2api-go/handlers.(*Handler).ListModels-fm (6 handlers)
15
+ [GIN-debug] POST /v1/chat/completions --> cursor2api-go/handlers.(*Handler).ChatCompletions-fm (6 handlers)
16
+ [GIN-debug] GET /static/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (5 handlers)
17
+ [GIN-debug] HEAD /static/*filepath --> github.com/gin-gonic/gin.(*RouterGroup).createStaticHandler.func1 (5 handlers)
18
+ INFO[0000] Starting Cursor2API server on port 8002
19
+ ```
20
+
21
+ ### 优化后(简洁模式,默认)
22
+ 启动时只显示必要的服务信息:
23
+ ```
24
+ ╔══════════════════════════════════════════════════════════════╗
25
+ ║ Cursor2API Server ║
26
+ ╚══════════════════════════════════════════════════════════════╝
27
+
28
+ 🚀 服务地址: http://localhost:8002
29
+ 📚 API 文档: http://localhost:8002/
30
+ 💊 健康检查: http://localhost:8002/health
31
+ 🔑 API 密钥: 0000
32
+ 🤖 支持模型: gpt-5.1 等 23 个模型
33
+
34
+ ✨ 服务已启动,按 Ctrl+C 停止
35
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
36
+ ```
37
+
38
+ ## 主要改进
39
+
40
+ ### 1. 默认使用简洁模式
41
+ - 修改 `.env.example` 中 `DEBUG=false`
42
+ - 生产环境默认不显示调试信息
43
+ - 启动输出更清爽、专业
44
+
45
+ ### 2. 美观的启动横幅
46
+ - 使用 Unicode 框线字符绘制横幅
47
+ - 使用 Emoji 图标增强可读性
48
+ - 清晰展示关键信息:
49
+ - 🚀 服务地址
50
+ - 📚 API 文档
51
+ - 💊 健康检查
52
+ - 🔑 API 密钥
53
+ - 🤖 支持的模型
54
+
55
+ ### 3. 条件性日志输出
56
+ - 只在 `DEBUG=true` 时显示详细日志
57
+ - GIN 的 Logger 中间件仅在调试模式启用
58
+ - 减少生产环境的日志噪音
59
+
60
+ ### 4. 智能模型显示
61
+ - 模型数量 > 3 时,只显示第一个和总数
62
+ - 避免启动信息过长
63
+ - 保持输出简洁
64
+
65
+ ## 代码改进
66
+
67
+ ### main.go
68
+ ```go
69
+ // 设置日志级别和 GIN 模式
70
+ if cfg.Debug {
71
+ logrus.SetLevel(logrus.DebugLevel)
72
+ gin.SetMode(gin.DebugMode)
73
+ } else {
74
+ logrus.SetLevel(logrus.InfoLevel)
75
+ gin.SetMode(gin.ReleaseMode)
76
+ }
77
+
78
+ // 只在 Debug 模式下启用 GIN 的日志
79
+ if cfg.Debug {
80
+ router.Use(gin.Logger())
81
+ }
82
+
83
+ // 打印启动信息
84
+ printStartupBanner(cfg)
85
+ ```
86
+
87
+ ### printStartupBanner 函数
88
+ ```go
89
+ func printStartupBanner(cfg *config.Config) {
90
+ banner := `
91
+ ╔══════════════════════════════════════════════════════════════╗
92
+ ║ Cursor2API Server ║
93
+ ╚══════════════════════════════════════════════════════════════╝
94
+ `
95
+ fmt.Println(banner)
96
+
97
+ fmt.Printf("🚀 服务地址: http://localhost:%d\n", cfg.Port)
98
+ fmt.Printf("📚 API 文档: http://localhost:%d/\n", cfg.Port)
99
+ fmt.Printf("💊 健康检查: http://localhost:%d/health\n", cfg.Port)
100
+ fmt.Printf("🔑 API 密钥: %s\n", cfg.APIKey)
101
+
102
+ models := cfg.GetModels()
103
+ if len(models) > 3 {
104
+ fmt.Printf("🤖 支持模型: %s 等 %d 个模型\n", models[0], len(models))
105
+ } else {
106
+ fmt.Printf("🤖 支持模型: %v\n", models)
107
+ }
108
+
109
+ if cfg.Debug {
110
+ fmt.Println("🐛 调试模式: 已启用")
111
+ }
112
+
113
+ fmt.Println("\n✨ 服务已启动,按 Ctrl+C 停止")
114
+ fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
115
+ }
116
+ ```
117
+
118
+ ## 使用方法
119
+
120
+ ### 简洁模式(默认)
121
+ ```bash
122
+ ./cursor2api-go
123
+ ```
124
+
125
+ 或者使用启动脚本:
126
+ ```bash
127
+ ./start.sh
128
+ ```
129
+
130
+ ### 调试模式
131
+
132
+ **方式 1**: 修改 `.env` 文件
133
+ ```bash
134
+ DEBUG=true
135
+ ```
136
+
137
+ **方式 2**: 临时启用
138
+ ```bash
139
+ DEBUG=true ./cursor2api-go
140
+ ```
141
+
142
+ ### 调试模式会显示
143
+ - ✅ 详细的 GIN 路由信息
144
+ - ✅ 每个请求的详细日志
145
+ - ✅ x-is-human token 信息
146
+ - ✅ 浏览器指纹配置
147
+ - ✅ 重试和错误处理详情
148
+
149
+ ## 优势
150
+
151
+ 1. **更专业** - 简洁的输出适合生产环境
152
+ 2. **更清晰** - 关键信息一目了然
153
+ 3. **更美观** - 使用 Unicode 和 Emoji 增强视觉效果
154
+ 4. **更灵活** - 可以随时切换调试模式
155
+ 5. **更友好** - 新用户更容易理解服务状态
156
+
157
+ ## 兼容性
158
+
159
+ - ✅ 完全向后兼容
160
+ - ✅ 不影响现有功能
161
+ - ✅ 可以随时切换模式
162
+ - ✅ 支持所有平台(Windows/macOS/Linux)
163
+
164
+ ## 注意事项
165
+
166
+ 1. **首次使用**: 需要更新 `.env` 文件(或删除后重新生成)
167
+ 2. **调试问题**: 遇到问题时,建议启用 `DEBUG=true` 查看详细日志
168
+ 3. **生产部署**: 建议使用 `DEBUG=false` 以获得最佳性能和简洁输出
go.mod ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module cursor2api-go
2
+
3
+ go 1.24
4
+
5
+ require (
6
+ github.com/gin-gonic/gin v1.10.0
7
+ github.com/imroc/req/v3 v3.55.0
8
+ github.com/joho/godotenv v1.5.1
9
+ github.com/sirupsen/logrus v1.9.3
10
+ )
11
+
12
+ require (
13
+ github.com/andybalholm/brotli v1.2.0 // indirect
14
+ github.com/bytedance/sonic v1.11.6 // indirect
15
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
16
+ github.com/cloudflare/circl v1.6.1 // indirect
17
+ github.com/cloudwego/base64x v0.1.4 // indirect
18
+ github.com/cloudwego/iasm v0.2.0 // indirect
19
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
20
+ github.com/gin-contrib/sse v0.1.0 // indirect
21
+ github.com/go-playground/locales v0.14.1 // indirect
22
+ github.com/go-playground/universal-translator v0.18.1 // indirect
23
+ github.com/go-playground/validator/v10 v10.20.0 // indirect
24
+ github.com/goccy/go-json v0.10.2 // indirect
25
+ github.com/google/go-querystring v1.1.0 // indirect
26
+ github.com/icholy/digest v1.1.0 // indirect
27
+ github.com/json-iterator/go v1.1.12 // indirect
28
+ github.com/klauspost/compress v1.18.0 // indirect
29
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
30
+ github.com/leodido/go-urn v1.4.0 // indirect
31
+ github.com/mattn/go-isatty v0.0.20 // indirect
32
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
33
+ github.com/modern-go/reflect2 v1.0.2 // indirect
34
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
35
+ github.com/quic-go/qpack v0.5.1 // indirect
36
+ github.com/quic-go/quic-go v0.53.0 // indirect
37
+ github.com/refraction-networking/utls v1.7.3 // indirect
38
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
39
+ github.com/ugorji/go/codec v1.2.12 // indirect
40
+ go.uber.org/mock v0.5.2 // indirect
41
+ golang.org/x/arch v0.8.0 // indirect
42
+ golang.org/x/crypto v0.39.0 // indirect
43
+ golang.org/x/mod v0.25.0 // indirect
44
+ golang.org/x/net v0.41.0 // indirect
45
+ golang.org/x/sync v0.15.0 // indirect
46
+ golang.org/x/sys v0.33.0 // indirect
47
+ golang.org/x/text v0.26.0 // indirect
48
+ golang.org/x/tools v0.34.0 // indirect
49
+ google.golang.org/protobuf v1.34.1 // indirect
50
+ gopkg.in/yaml.v3 v3.0.1 // indirect
51
+ )
go.sum ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
2
+ github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
3
+ github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
4
+ github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
5
+ github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
6
+ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
7
+ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
8
+ github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
9
+ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
10
+ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
11
+ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
12
+ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
13
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
15
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16
+ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
17
+ github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
18
+ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
19
+ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
20
+ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
21
+ github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
22
+ github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
23
+ github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
24
+ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
25
+ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
26
+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
27
+ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
28
+ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
29
+ github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
30
+ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
31
+ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
32
+ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
33
+ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
34
+ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
35
+ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
36
+ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
37
+ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
38
+ github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
39
+ github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
40
+ github.com/imroc/req/v3 v3.55.0 h1:vg2Q33TGU12wZWZyPkiPbCGGTeiOmlEOdOwHLH03//I=
41
+ github.com/imroc/req/v3 v3.55.0/go.mod h1:MOn++r2lE4+du3nuefTaPGQ6pY3/yRP2r1pFK1BUqq0=
42
+ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
43
+ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
44
+ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
45
+ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
46
+ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
47
+ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
48
+ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
49
+ github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
50
+ github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
51
+ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
52
+ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
53
+ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
54
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
55
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
56
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
57
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
58
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
59
+ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
60
+ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
61
+ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
62
+ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
63
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
64
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
65
+ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
66
+ github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
67
+ github.com/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI=
68
+ github.com/quic-go/quic-go v0.53.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
69
+ github.com/refraction-networking/utls v1.7.3 h1:L0WRhHY7Oq1T0zkdzVZMR6zWZv+sXbHB9zcuvsAEqCo=
70
+ github.com/refraction-networking/utls v1.7.3/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ=
71
+ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
72
+ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
73
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
74
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
75
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
76
+ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
77
+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
78
+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
79
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
80
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
81
+ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
82
+ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
83
+ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
84
+ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
85
+ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
86
+ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
87
+ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
88
+ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
89
+ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
90
+ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
91
+ go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
92
+ go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
93
+ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
94
+ golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
95
+ golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
96
+ golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
97
+ golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
98
+ golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
99
+ golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
100
+ golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
101
+ golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
102
+ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
103
+ golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
104
+ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
105
+ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
106
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
107
+ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
108
+ golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
109
+ golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
110
+ golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
111
+ golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
112
+ golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
113
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
114
+ google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
115
+ google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
116
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
117
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
118
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
119
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
120
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
121
+ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
122
+ gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
123
+ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
124
+ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
handlers/handler.go ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package handlers
22
+
23
+ import (
24
+ "cursor2api-go/config"
25
+ "cursor2api-go/middleware"
26
+ "cursor2api-go/models"
27
+ "cursor2api-go/services"
28
+ "cursor2api-go/utils"
29
+ "net/http"
30
+ "os"
31
+ "time"
32
+
33
+ "github.com/gin-gonic/gin"
34
+ "github.com/sirupsen/logrus"
35
+ )
36
+
37
+ // Handler 处理器结构
38
+ type Handler struct {
39
+ config *config.Config
40
+ cursorService *services.CursorService
41
+ docsContent []byte
42
+ }
43
+
44
+ // NewHandler 创建新的处理器
45
+ func NewHandler(cfg *config.Config) *Handler {
46
+ cursorService := services.NewCursorService(cfg)
47
+
48
+ // 预加载文档内容
49
+ docsPath := "static/docs.html"
50
+ var docsContent []byte
51
+
52
+ if data, err := os.ReadFile(docsPath); err == nil {
53
+ docsContent = data
54
+ } else {
55
+ // 如果文件不存在,使用默认的简单HTML页面
56
+ simpleHTML := `
57
+ <!DOCTYPE html>
58
+ <html lang="en">
59
+ <head>
60
+ <meta charset="UTF-8">
61
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
62
+ <title>Cursor2API - Go Version</title>
63
+ <style>
64
+ body {
65
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
66
+ max-width: 800px;
67
+ margin: 50px auto;
68
+ padding: 20px;
69
+ background-color: #f5f5f5;
70
+ }
71
+ .container {
72
+ background: white;
73
+ padding: 30px;
74
+ border-radius: 10px;
75
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
76
+ }
77
+ h1 {
78
+ color: #333;
79
+ border-bottom: 2px solid #007bff;
80
+ padding-bottom: 10px;
81
+ }
82
+ .info {
83
+ background: #f8f9fa;
84
+ padding: 20px;
85
+ border-radius: 8px;
86
+ margin: 20px 0;
87
+ border-left: 4px solid #007bff;
88
+ }
89
+ code {
90
+ background: #e9ecef;
91
+ padding: 2px 6px;
92
+ border-radius: 4px;
93
+ font-family: 'Courier New', monospace;
94
+ }
95
+ .endpoint {
96
+ background: #e3f2fd;
97
+ padding: 10px;
98
+ margin: 10px 0;
99
+ border-radius: 5px;
100
+ border-left: 3px solid #2196f3;
101
+ }
102
+ .status-ok {
103
+ color: #28a745;
104
+ font-weight: bold;
105
+ }
106
+ </style>
107
+ </head>
108
+ <body>
109
+ <div class="container">
110
+ <h1>🚀 Cursor2API - Go Version</h1>
111
+
112
+ <div class="info">
113
+ <p><strong>Status:</strong> <span class="status-ok">✅ Running</span></p>
114
+ <p><strong>Version:</strong> Go Implementation</p>
115
+ <p><strong>Description:</strong> OpenAI-compatible API proxy for Cursor AI</p>
116
+ </div>
117
+
118
+ <div class="info">
119
+ <h3>📡 Available Endpoints:</h3>
120
+ <div class="endpoint">
121
+ <strong>GET</strong> <code>/v1/models</code><br>
122
+ <small>List available AI models</small>
123
+ </div>
124
+ <div class="endpoint">
125
+ <strong>POST</strong> <code>/v1/chat/completions</code><br>
126
+ <small>Create chat completion (supports streaming)</small>
127
+ </div>
128
+ <div class="endpoint">
129
+ <strong>GET</strong> <code>/health</code><br>
130
+ <small>Health check endpoint</small>
131
+ </div>
132
+ </div>
133
+
134
+ <div class="info">
135
+ <h3>🔐 Authentication:</h3>
136
+ <p>Use Bearer token authentication:</p>
137
+ <code>Authorization: Bearer YOUR_API_KEY</code>
138
+ <p><small>Default API key: <code>0000</code> (change via API_KEY environment variable)</small></p>
139
+ </div>
140
+
141
+ <div class="info">
142
+ <h3>💻 Example Usage:</h3>
143
+ <pre><code>curl -X POST http://localhost:8002/v1/chat/completions \
144
+ -H "Content-Type: application/json" \
145
+ -H "Authorization: Bearer 0000" \
146
+ -d '{
147
+ "model": "claude-sonnet-4.6",
148
+ "messages": [
149
+ {"role": "user", "content": "Hello!"}
150
+ ]
151
+ }'</code></pre>
152
+ </div>
153
+
154
+ <div class="info">
155
+ <p><strong>Repository:</strong> <a href="https://github.com/cursor2api/cursor2api-go">cursor2api-go</a></p>
156
+ <p><strong>Documentation:</strong> OpenAI API compatible</p>
157
+ </div>
158
+ </div>
159
+ </body>
160
+ </html>`
161
+ docsContent = []byte(simpleHTML)
162
+ }
163
+
164
+ return &Handler{
165
+ config: cfg,
166
+ cursorService: cursorService,
167
+ docsContent: docsContent,
168
+ }
169
+
170
+ }
171
+
172
+ // ListModels 列出可用模型
173
+ func (h *Handler) ListModels(c *gin.Context) {
174
+ modelNames := h.config.GetModels()
175
+ modelList := make([]models.Model, 0, len(modelNames))
176
+
177
+ for _, modelID := range modelNames {
178
+ // 获取模型配置信息
179
+ modelConfig, exists := models.GetModelConfig(modelID)
180
+
181
+ model := models.Model{
182
+ ID: modelID,
183
+ Object: "model",
184
+ Created: time.Now().Unix(),
185
+ OwnedBy: "cursor2api",
186
+ }
187
+
188
+ // 如果找到模型配置,添加max_tokens和context_window信息
189
+ if exists {
190
+ model.MaxTokens = modelConfig.MaxTokens
191
+ model.ContextWindow = modelConfig.ContextWindow
192
+ }
193
+
194
+ modelList = append(modelList, model)
195
+ }
196
+
197
+ response := models.ModelsResponse{
198
+ Object: "list",
199
+ Data: modelList,
200
+ }
201
+
202
+ c.JSON(http.StatusOK, response)
203
+ }
204
+
205
+ // ChatCompletions 处理聊天完成请求
206
+ func (h *Handler) ChatCompletions(c *gin.Context) {
207
+ var request models.ChatCompletionRequest
208
+ if err := c.ShouldBindJSON(&request); err != nil {
209
+ logrus.WithError(err).Error("Failed to bind request")
210
+ c.JSON(http.StatusBadRequest, models.NewErrorResponse(
211
+ "Invalid request format",
212
+ "invalid_request_error",
213
+ "invalid_json",
214
+ ))
215
+ return
216
+ }
217
+
218
+ // 验证模型
219
+ if !h.config.IsValidModel(request.Model) {
220
+ c.JSON(http.StatusBadRequest, models.NewErrorResponse(
221
+ "Invalid model specified",
222
+ "invalid_request_error",
223
+ "model_not_found",
224
+ ))
225
+ return
226
+ }
227
+
228
+ // 验证消息
229
+ if len(request.Messages) == 0 {
230
+ c.JSON(http.StatusBadRequest, models.NewErrorResponse(
231
+ "Messages cannot be empty",
232
+ "invalid_request_error",
233
+ "missing_messages",
234
+ ))
235
+ return
236
+ }
237
+
238
+ // 验证并调整max_tokens参数
239
+ request.MaxTokens = models.ValidateMaxTokens(request.Model, request.MaxTokens)
240
+
241
+ // 调用Cursor服务
242
+ chatGenerator, err := h.cursorService.ChatCompletion(c.Request.Context(), &request)
243
+ if err != nil {
244
+ logrus.WithError(err).Error("Failed to create chat completion")
245
+ middleware.HandleError(c, err)
246
+ return
247
+ }
248
+
249
+ // 根据是否流式返回不同响应
250
+ if request.Stream {
251
+ utils.SafeStreamWrapper(utils.StreamChatCompletion, c, chatGenerator, request.Model)
252
+ } else {
253
+ utils.NonStreamChatCompletion(c, chatGenerator, request.Model)
254
+ }
255
+ }
256
+
257
+ // ServeDocs 服务API文档页面
258
+ func (h *Handler) ServeDocs(c *gin.Context) {
259
+ c.Data(http.StatusOK, "text/html; charset=utf-8", h.docsContent)
260
+ }
261
+
262
+ // Health 健康检查
263
+ func (h *Handler) Health(c *gin.Context) {
264
+ c.JSON(http.StatusOK, gin.H{
265
+ "status": "ok",
266
+ "timestamp": time.Now().Unix(),
267
+ "version": "go-1.0.0",
268
+ })
269
+ }
jscode/env.js ADDED
The diff for this file is too large to render. See raw diff
 
jscode/main.js ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ global.cursor_config = {
2
+ currentScriptSrc: "$$currentScriptSrc$$",
3
+ fp:{
4
+ UNMASKED_VENDOR_WEBGL:"$$UNMASKED_VENDOR_WEBGL$$",
5
+ UNMASKED_RENDERER_WEBGL:"$$UNMASKED_RENDERER_WEBGL$$",
6
+ userAgent: "$$userAgent$$"
7
+ }
8
+ }
9
+
10
+ $$env_jscode$$
11
+
12
+ let console_log = console.log;
13
+ console.log = function () {
14
+
15
+ }
16
+
17
+ dtavm = console;
18
+ delete __dirname;
19
+ delete __filename;
20
+
21
+ function proxy(obj, objname, type) {
22
+ function getMethodHandler(WatchName, target_obj) {
23
+ let methodhandler = {
24
+ apply(target, thisArg, argArray) {
25
+ if (this.target_obj) {
26
+ thisArg = this.target_obj
27
+ }
28
+ let result = Reflect.apply(target, thisArg, argArray)
29
+ if (target.name !== "toString") {
30
+ if (target.name === "addEventListener") {
31
+ dtavm.log(`调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray[0]}], 结果 => [${result}].`)
32
+ } else if (WatchName === "window.console") {
33
+ } else {
34
+ dtavm.log(`调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${result}].`)
35
+ }
36
+ } else {
37
+ dtavm.log(`调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${result}].`)
38
+ }
39
+ return result
40
+ },
41
+ construct(target, argArray, newTarget) {
42
+ var result = Reflect.construct(target, argArray, newTarget)
43
+ dtavm.log(`调用者 => [${WatchName}] 构造函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${(result)}].`)
44
+ return result;
45
+ }
46
+ }
47
+ methodhandler.target_obj = target_obj
48
+ return methodhandler
49
+ }
50
+
51
+ function getObjhandler(WatchName) {
52
+ let handler = {
53
+ get(target, propKey, receiver) {
54
+ let result = target[propKey]
55
+ if (result instanceof Object) {
56
+ if (typeof result === "function") {
57
+ dtavm.log(`调用者 => [${WatchName}] 获取属性名 => [${propKey}] , 是个函数`)
58
+ return new Proxy(result, getMethodHandler(WatchName, target))
59
+ } else {
60
+ dtavm.log(`调用者 => [${WatchName}] 获取属性名 => [${propKey}], 结果 => [${(result)}]`);
61
+ }
62
+ return new Proxy(result, getObjhandler(`${WatchName}.${propKey}`))
63
+ }
64
+ if (typeof (propKey) !== "symbol") {
65
+ dtavm.log(`调用者 => [${WatchName}] 获取属性名 => [${propKey?.description ?? propKey}], 结果 => [${result}]`);
66
+ }
67
+ return result;
68
+ },
69
+ set(target, propKey, value, receiver) {
70
+ if (value instanceof Object) {
71
+ dtavm.log(`调用者 => [${WatchName}] 设置属性名 => [${propKey}], 值为 => [${(value)}]`);
72
+ } else {
73
+ dtavm.log(`调用者 => [${WatchName}] 设置属性名 => [${propKey}], 值为 => [${value}]`);
74
+ }
75
+ return Reflect.set(target, propKey, value, receiver);
76
+ },
77
+ has(target, propKey) {
78
+ var result = Reflect.has(target, propKey);
79
+ dtavm.log(`针对in操作符的代理has=> [${WatchName}] 有无属性名 => [${propKey}], 结果 => [${result}]`)
80
+ return result;
81
+ },
82
+ deleteProperty(target, propKey) {
83
+ var result = Reflect.deleteProperty(target, propKey);
84
+ dtavm.log(`拦截属性delete => [${WatchName}] 删除属性名 => [${propKey}], 结果 => [${result}]`)
85
+ return result;
86
+ },
87
+ defineProperty(target, propKey, attributes) {
88
+ var result = Reflect.defineProperty(target, propKey, attributes);
89
+ dtavm.log(`拦截对象define操作 => [${WatchName}] 待检索属性名 => [${propKey.toString()}] 属性描述 => [${(attributes)}], 结果 => [${result}]`)
90
+ // debugger
91
+ return result
92
+ },
93
+ getPrototypeOf(target) {
94
+ var result = Reflect.getPrototypeOf(target)
95
+ dtavm.log(`被代理的目标对象 => [${WatchName}] 代理结果 => [${(result)}]`)
96
+ return result;
97
+ },
98
+ setPrototypeOf(target, proto) {
99
+ dtavm.log(`被拦截的目标对象 => [${WatchName}] 对象新原型==> [${(proto)}]`)
100
+ return Reflect.setPrototypeOf(target, proto);
101
+ },
102
+ preventExtensions(target) {
103
+ dtavm.log(`方法用于设置preventExtensions => [${WatchName}] 防止扩展`)
104
+ return Reflect.preventExtensions(target);
105
+ },
106
+ isExtensible(target) {
107
+ var result = Reflect.isExtensible(target)
108
+ dtavm.log(`拦截对对象的isExtensible() => [${WatchName}] isExtensible, 返回值==> [${result}]`)
109
+ return result;
110
+ },
111
+ }
112
+ return handler;
113
+ }
114
+
115
+ if (type === "method") {
116
+ return new Proxy(obj, getMethodHandler(objname, obj));
117
+ }
118
+ return new Proxy(obj, getObjhandler(objname));
119
+ }
120
+
121
+ // window = proxy(window, 'window');
122
+ global.document = window.document;
123
+
124
+ $$cursor_jscode$$
125
+
126
+
127
+ window.V_C[0]().then(value => console_log(JSON.stringify(value)));
128
+
main.go ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "cursor2api-go/config"
6
+ "cursor2api-go/handlers"
7
+ "cursor2api-go/middleware"
8
+ "cursor2api-go/models"
9
+ "fmt"
10
+ "net/http"
11
+ "os"
12
+ "os/signal"
13
+ "strings"
14
+ "syscall"
15
+ "time"
16
+
17
+ "github.com/gin-gonic/gin"
18
+ "github.com/sirupsen/logrus"
19
+ )
20
+
21
+ func main() {
22
+ // 加载配置
23
+ cfg, err := config.LoadConfig()
24
+ if err != nil {
25
+ logrus.Fatalf("Failed to load config: %v", err)
26
+ }
27
+
28
+ // 设置日志级别和 GIN 模式(必须在创建路由器之前设置)
29
+ if cfg.Debug {
30
+ logrus.SetLevel(logrus.DebugLevel)
31
+ gin.SetMode(gin.DebugMode)
32
+ } else {
33
+ logrus.SetLevel(logrus.InfoLevel)
34
+ gin.SetMode(gin.ReleaseMode)
35
+ }
36
+
37
+ // 禁用 Gin 的调试信息输出
38
+ gin.DisableConsoleColor()
39
+
40
+ // 创建路由器(使用 gin.New() 而不是 gin.Default() 以避免默认日志)
41
+ router := gin.New()
42
+
43
+ // 添加中间件
44
+ router.Use(gin.Recovery())
45
+ router.Use(middleware.CORS())
46
+ router.Use(middleware.ErrorHandler())
47
+ // 只在 Debug 模式下启用 GIN 的日志
48
+ if cfg.Debug {
49
+ router.Use(gin.Logger())
50
+ }
51
+
52
+ // 创建处理器
53
+ handler := handlers.NewHandler(cfg)
54
+
55
+ // 注册路由
56
+ setupRoutes(router, handler)
57
+
58
+ // 创建HTTP服务器
59
+ server := &http.Server{
60
+ Addr: fmt.Sprintf(":%d", cfg.Port),
61
+ Handler: router,
62
+ }
63
+
64
+ // 打印启动信息
65
+ printStartupBanner(cfg)
66
+
67
+ // 启动服务器的goroutine
68
+ go func() {
69
+ if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
70
+ logrus.Fatalf("Failed to start server: %v", err)
71
+ }
72
+ }()
73
+
74
+ // 等待中断信号以优雅关闭服务器
75
+ quit := make(chan os.Signal, 1)
76
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
77
+ <-quit
78
+ logrus.Info("Shutting down server...")
79
+
80
+ // 给服务器5秒时间完成处理正在进行的请求
81
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
82
+ defer cancel()
83
+ if err := server.Shutdown(ctx); err != nil {
84
+ logrus.Fatalf("Server forced to shutdown: %v", err)
85
+ }
86
+
87
+ logrus.Info("Server exited")
88
+ }
89
+
90
+ func setupRoutes(router *gin.Engine, handler *handlers.Handler) {
91
+ // 健康检查
92
+ router.GET("/health", func(c *gin.Context) {
93
+ c.JSON(http.StatusOK, gin.H{
94
+ "status": "ok",
95
+ "time": time.Now().Unix(),
96
+ })
97
+ })
98
+
99
+ // API文档页面
100
+ router.GET("/", handler.ServeDocs)
101
+
102
+ // API v1路由组
103
+ v1 := router.Group("/v1")
104
+ {
105
+ // 模型列表
106
+ v1.GET("/models", middleware.AuthRequired(), handler.ListModels)
107
+
108
+ // 聊天完成
109
+ v1.POST("/chat/completions", middleware.AuthRequired(), handler.ChatCompletions)
110
+ }
111
+
112
+ // 静态文件服务(如果需要)
113
+ router.Static("/static", "./static")
114
+ }
115
+
116
+ // printStartupBanner 打印启动横幅
117
+ func printStartupBanner(cfg *config.Config) {
118
+ banner := `
119
+ ╔══════════════════════════════════════════════════════════════╗
120
+ ║ Cursor2API Server ║
121
+ ╚══════════════════════════════════════════════════════════════╝
122
+ `
123
+ fmt.Println(banner)
124
+
125
+ fmt.Printf("🚀 服务地址: http://localhost:%d\n", cfg.Port)
126
+ fmt.Printf("📚 API 文档: http://localhost:%d/\n", cfg.Port)
127
+ fmt.Printf("💊 健康检查: http://localhost:%d/health\n", cfg.Port)
128
+ fmt.Printf("🔑 API 密钥: %s\n", maskAPIKey(cfg.APIKey))
129
+
130
+ modelList := cfg.GetModels()
131
+ fmt.Printf("\n🤖 支持模型 (%d 个):\n", len(modelList))
132
+
133
+ // 按类别分组显示模型
134
+ providers := make(map[string][]string)
135
+ for _, modelID := range modelList {
136
+ if config, exists := models.GetModelConfig(modelID); exists {
137
+ providers[config.Provider] = append(providers[config.Provider], modelID)
138
+ } else {
139
+ providers["Other"] = append(providers["Other"], modelID)
140
+ }
141
+ }
142
+
143
+ // 按Provider排序并显示
144
+ for _, provider := range []string{"Anthropic", "OpenAI", "Google", "Other"} {
145
+ if models, ok := providers[provider]; ok && len(models) > 0 {
146
+ fmt.Printf(" %s: %s\n", provider, strings.Join(models, ", "))
147
+ }
148
+ }
149
+
150
+ if cfg.Debug {
151
+ fmt.Println("\n🐛 调试模式: 已启用")
152
+ }
153
+
154
+ fmt.Println("\n✨ 服务已启动,按 Ctrl+C 停止")
155
+ fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
156
+ }
157
+
158
+ // maskAPIKey 掩码 API 密钥,只显示前 4 位
159
+ func maskAPIKey(key string) string {
160
+ if len(key) <= 4 {
161
+ return "****"
162
+ }
163
+ return key[:4] + "****"
164
+ }
middleware/auth.go ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package middleware
22
+
23
+ import (
24
+ "cursor2api-go/models"
25
+ "net/http"
26
+ "os"
27
+ "strings"
28
+
29
+ "github.com/gin-gonic/gin"
30
+ )
31
+
32
+ // AuthRequired 认证中间件
33
+ func AuthRequired() gin.HandlerFunc {
34
+ return func(c *gin.Context) {
35
+ authHeader := c.GetHeader("Authorization")
36
+
37
+ if authHeader == "" {
38
+ errorResponse := models.NewErrorResponse(
39
+ "Missing authorization header",
40
+ "authentication_error",
41
+ "missing_auth",
42
+ )
43
+ c.JSON(http.StatusUnauthorized, errorResponse)
44
+ c.Abort()
45
+ return
46
+ }
47
+
48
+ if !strings.HasPrefix(authHeader, "Bearer ") {
49
+ errorResponse := models.NewErrorResponse(
50
+ "Invalid authorization format. Expected 'Bearer <token>'",
51
+ "authentication_error",
52
+ "invalid_auth_format",
53
+ )
54
+ c.JSON(http.StatusUnauthorized, errorResponse)
55
+ c.Abort()
56
+ return
57
+ }
58
+
59
+ token := strings.TrimPrefix(authHeader, "Bearer ")
60
+ expectedToken := os.Getenv("API_KEY")
61
+ if expectedToken == "" {
62
+ expectedToken = "0000" // 默认值
63
+ }
64
+
65
+ if token != expectedToken {
66
+ errorResponse := models.NewErrorResponse(
67
+ "Invalid API key",
68
+ "authentication_error",
69
+ "invalid_api_key",
70
+ )
71
+ c.JSON(http.StatusUnauthorized, errorResponse)
72
+ c.Abort()
73
+ return
74
+ }
75
+
76
+ // 认证通过,继续处理请求
77
+ c.Next()
78
+ }
79
+ }
middleware/cors.go ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package middleware
22
+
23
+ import (
24
+ "github.com/gin-gonic/gin"
25
+ )
26
+
27
+ // CORS 跨域中间件
28
+ func CORS() gin.HandlerFunc {
29
+ return func(c *gin.Context) {
30
+ // 设置CORS头
31
+ c.Header("Access-Control-Allow-Origin", "*")
32
+ c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE")
33
+ c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
34
+ c.Header("Access-Control-Allow-Credentials", "true")
35
+ c.Header("Access-Control-Max-Age", "86400")
36
+
37
+ // 处理预检请求
38
+ if c.Request.Method == "OPTIONS" {
39
+ c.AbortWithStatus(200)
40
+ return
41
+ }
42
+
43
+ c.Next()
44
+ }
45
+ }
middleware/error.go ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package middleware
22
+
23
+ import (
24
+ "cursor2api-go/models"
25
+ "net/http"
26
+
27
+ "github.com/gin-gonic/gin"
28
+ "github.com/sirupsen/logrus"
29
+ )
30
+
31
+ // CursorWebError Cursor Web API错误
32
+ type CursorWebError struct {
33
+ StatusCode int `json:"status_code"`
34
+ Message string `json:"message"`
35
+ }
36
+
37
+ // Error 实现error接口
38
+ func (e *CursorWebError) Error() string {
39
+ return e.Message
40
+ }
41
+
42
+ // NewCursorWebError 创建新的CursorWebError
43
+ func NewCursorWebError(statusCode int, message string) *CursorWebError {
44
+ return &CursorWebError{
45
+ StatusCode: statusCode,
46
+ Message: message,
47
+ }
48
+ }
49
+
50
+ // ErrorHandler 全局错误处理中间件
51
+ func ErrorHandler() gin.HandlerFunc {
52
+ return func(c *gin.Context) {
53
+ c.Next()
54
+
55
+ // 处理上下文中的错误
56
+ if len(c.Errors) > 0 {
57
+ err := c.Errors.Last().Err
58
+ handleError(c, err)
59
+ }
60
+ }
61
+ }
62
+
63
+ // HandleError 处理错误并返回适当的响应
64
+ func HandleError(c *gin.Context, err error) {
65
+ handleError(c, err)
66
+ }
67
+
68
+ // handleError 内部错误处理逻辑
69
+ func handleError(c *gin.Context, err error) {
70
+ // 如果已经写入了响应头,则不再处理
71
+ if c.Writer.Written() {
72
+ return
73
+ }
74
+
75
+ logrus.WithError(err).Error("API error occurred")
76
+
77
+ switch e := err.(type) {
78
+ case *CursorWebError:
79
+ // 处理Cursor Web错误
80
+ errorResponse := models.NewErrorResponse(
81
+ e.Message,
82
+ "cursor_web_error",
83
+ "",
84
+ )
85
+ c.JSON(e.StatusCode, errorResponse)
86
+
87
+ case *gin.Error:
88
+ // 处理Gin绑定错误
89
+ statusCode := http.StatusBadRequest
90
+ if e.Type == gin.ErrorTypePublic {
91
+ statusCode = http.StatusInternalServerError
92
+ }
93
+
94
+ errorResponse := models.NewErrorResponse(
95
+ e.Error(),
96
+ "validation_error",
97
+ "invalid_request",
98
+ )
99
+ c.JSON(statusCode, errorResponse)
100
+
101
+ default:
102
+ // 处理其他错误
103
+ errorResponse := models.NewErrorResponse(
104
+ "Internal server error",
105
+ "internal_error",
106
+ "",
107
+ )
108
+ c.JSON(http.StatusInternalServerError, errorResponse)
109
+ }
110
+ }
111
+
112
+ // RecoveryHandler 自定义恢复中间件
113
+ func RecoveryHandler() gin.HandlerFunc {
114
+ return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
115
+ logrus.WithField("panic", recovered).Error("Panic occurred")
116
+
117
+ if c.Writer.Written() {
118
+ return
119
+ }
120
+
121
+ errorResponse := models.NewErrorResponse(
122
+ "Internal server error",
123
+ "panic_error",
124
+ "",
125
+ )
126
+ c.JSON(http.StatusInternalServerError, errorResponse)
127
+ })
128
+ }
129
+
130
+ // ValidationError 验证错误
131
+ type ValidationError struct {
132
+ Field string `json:"field"`
133
+ Message string `json:"message"`
134
+ }
135
+
136
+ // MultipleValidationError 多个验证错误
137
+ type MultipleValidationError struct {
138
+ Errors []ValidationError `json:"errors"`
139
+ }
140
+
141
+ // Error 实现error接口
142
+ func (e *MultipleValidationError) Error() string {
143
+ return "validation failed"
144
+ }
145
+
146
+ // NewValidationError 创建验证错误
147
+ func NewValidationError(field, message string) *ValidationError {
148
+ return &ValidationError{
149
+ Field: field,
150
+ Message: message,
151
+ }
152
+ }
153
+
154
+ // AuthenticationError 认证错误
155
+ type AuthenticationError struct {
156
+ Message string `json:"message"`
157
+ }
158
+
159
+ // Error 实现error接口
160
+ func (e *AuthenticationError) Error() string {
161
+ return e.Message
162
+ }
163
+
164
+ // NewAuthenticationError 创建认证错误
165
+ func NewAuthenticationError(message string) *AuthenticationError {
166
+ return &AuthenticationError{
167
+ Message: message,
168
+ }
169
+ }
170
+
171
+ // RateLimitError 限流错误
172
+ type RateLimitError struct {
173
+ Message string `json:"message"`
174
+ RetryAfter int `json:"retry_after"`
175
+ }
176
+
177
+ // Error 实现error接口
178
+ func (e *RateLimitError) Error() string {
179
+ return e.Message
180
+ }
181
+
182
+ // NewRateLimitError 创建限流错误
183
+ func NewRateLimitError(message string, retryAfter int) *RateLimitError {
184
+ return &RateLimitError{
185
+ Message: message,
186
+ RetryAfter: retryAfter,
187
+ }
188
+ }
models/model_config.go ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package models
22
+
23
+ // ModelConfig 模型配置结构
24
+ type ModelConfig struct {
25
+ ID string `json:"id"`
26
+ Provider string `json:"provider"`
27
+ MaxTokens int `json:"max_tokens"`
28
+ ContextWindow int `json:"context_window"`
29
+ CursorModel string `json:"cursor_model"` // Cursor API 使用的实际模型名
30
+ }
31
+
32
+ // GetModelConfigs 获取所有模型配置
33
+ func GetModelConfigs() map[string]ModelConfig {
34
+ return map[string]ModelConfig{
35
+ "claude-sonnet-4.6": {
36
+ ID: "claude-sonnet-4.6",
37
+ Provider: "Anthropic",
38
+ MaxTokens: 8192,
39
+ ContextWindow: 200000,
40
+ CursorModel: "anthropic/claude-sonnet-4.6",
41
+ },
42
+ }
43
+ }
44
+
45
+ // GetModelConfig 获取指定模型的配置
46
+ func GetModelConfig(modelID string) (ModelConfig, bool) {
47
+ configs := GetModelConfigs()
48
+ config, exists := configs[modelID]
49
+ return config, exists
50
+ }
51
+
52
+ // GetCursorModel 获取Cursor API使用的模型名称
53
+ func GetCursorModel(modelID string) string {
54
+ if config, exists := GetModelConfig(modelID); exists {
55
+ if config.CursorModel != "" {
56
+ return config.CursorModel
57
+ }
58
+ }
59
+ // 如果没有配置映射,返回原始模型名
60
+ return modelID
61
+ }
62
+
63
+ // GetMaxTokensForModel 获取指定模型的最大token数
64
+ func GetMaxTokensForModel(modelID string) int {
65
+ if config, exists := GetModelConfig(modelID); exists {
66
+ return config.MaxTokens
67
+ }
68
+ // 默认返回4096
69
+ return 4096
70
+ }
71
+
72
+ // GetContextWindowForModel 获取指定模型的上下文窗口大小
73
+ func GetContextWindowForModel(modelID string) int {
74
+ if config, exists := GetModelConfig(modelID); exists {
75
+ return config.ContextWindow
76
+ }
77
+ // 默认返回128000
78
+ return 128000
79
+ }
80
+
81
+ // ValidateMaxTokens 验证并调整max_tokens参数
82
+ func ValidateMaxTokens(modelID string, requestedMaxTokens *int) *int {
83
+ modelMaxTokens := GetMaxTokensForModel(modelID)
84
+
85
+ // 如果没有指定max_tokens,使用模型默认值
86
+ if requestedMaxTokens == nil {
87
+ return &modelMaxTokens
88
+ }
89
+
90
+ // 如果请求的max_tokens超过模型限制,使用模型最大值
91
+ if *requestedMaxTokens > modelMaxTokens {
92
+ return &modelMaxTokens
93
+ }
94
+
95
+ // 如果请求的max_tokens小于等于0,使用模型默认值
96
+ if *requestedMaxTokens <= 0 {
97
+ return &modelMaxTokens
98
+ }
99
+
100
+ return requestedMaxTokens
101
+ }
models/models.go ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package models
22
+
23
+ import (
24
+ "encoding/json"
25
+ "time"
26
+ )
27
+
28
+ // ChatCompletionRequest OpenAI聊天完成请求
29
+ type ChatCompletionRequest struct {
30
+ Model string `json:"model" binding:"required"`
31
+ Messages []Message `json:"messages" binding:"required"`
32
+ Stream bool `json:"stream,omitempty"`
33
+ Temperature *float64 `json:"temperature,omitempty"`
34
+ MaxTokens *int `json:"max_tokens,omitempty"`
35
+ TopP *float64 `json:"top_p,omitempty"`
36
+ Stop []string `json:"stop,omitempty"`
37
+ User string `json:"user,omitempty"`
38
+ }
39
+
40
+ // Message 消息结构
41
+ type Message struct {
42
+ Role string `json:"role" binding:"required"`
43
+ Content interface{} `json:"content" binding:"required"`
44
+ ToolCallID *string `json:"tool_call_id,omitempty"`
45
+ ToolCalls []ToolCall `json:"tool_calls,omitempty"`
46
+ }
47
+
48
+ // ToolCall 工具调用结构
49
+ type ToolCall struct {
50
+ ID string `json:"id"`
51
+ Type string `json:"type"`
52
+ Function Function `json:"function"`
53
+ }
54
+
55
+ // Function 函数调用结构
56
+ type Function struct {
57
+ Name string `json:"name"`
58
+ Arguments string `json:"arguments"`
59
+ }
60
+
61
+ // ContentPart 消息内容部分(用于多模态内容)
62
+ type ContentPart struct {
63
+ Type string `json:"type"`
64
+ Text string `json:"text,omitempty"`
65
+ URL string `json:"url,omitempty"`
66
+ }
67
+
68
+ // ChatCompletionResponse OpenAI聊天完成响应
69
+ type ChatCompletionResponse struct {
70
+ ID string `json:"id"`
71
+ Object string `json:"object"`
72
+ Created int64 `json:"created"`
73
+ Model string `json:"model"`
74
+ Choices []Choice `json:"choices"`
75
+ Usage Usage `json:"usage"`
76
+ }
77
+
78
+ // ChatCompletionStreamResponse 流式响应
79
+ type ChatCompletionStreamResponse struct {
80
+ ID string `json:"id"`
81
+ Object string `json:"object"`
82
+ Created int64 `json:"created"`
83
+ Model string `json:"model"`
84
+ Choices []StreamChoice `json:"choices"`
85
+ }
86
+
87
+ // Choice 选择结构
88
+ type Choice struct {
89
+ Index int `json:"index"`
90
+ Message Message `json:"message"`
91
+ FinishReason string `json:"finish_reason"`
92
+ }
93
+
94
+ // StreamChoice 流式选择结构
95
+ type StreamChoice struct {
96
+ Index int `json:"index"`
97
+ Delta StreamDelta `json:"delta"`
98
+ FinishReason *string `json:"finish_reason"`
99
+ }
100
+
101
+ // StreamDelta 流式增量数据
102
+ type StreamDelta struct {
103
+ Role string `json:"role,omitempty"`
104
+ Content string `json:"content,omitempty"`
105
+ }
106
+
107
+ // Usage 使用统计
108
+ type Usage struct {
109
+ PromptTokens int `json:"prompt_tokens"`
110
+ CompletionTokens int `json:"completion_tokens"`
111
+ TotalTokens int `json:"total_tokens"`
112
+ }
113
+
114
+ // Model 模型信息
115
+ type Model struct {
116
+ ID string `json:"id"`
117
+ Object string `json:"object"`
118
+ Created int64 `json:"created"`
119
+ OwnedBy string `json:"owned_by"`
120
+ MaxTokens int `json:"max_tokens,omitempty"`
121
+ ContextWindow int `json:"context_window,omitempty"`
122
+ }
123
+
124
+ // ModelsResponse 模型列表响应
125
+ type ModelsResponse struct {
126
+ Object string `json:"object"`
127
+ Data []Model `json:"data"`
128
+ }
129
+
130
+ // ErrorResponse 错误响应
131
+ type ErrorResponse struct {
132
+ Error ErrorDetail `json:"error"`
133
+ }
134
+
135
+ // ErrorDetail 错误详情
136
+ type ErrorDetail struct {
137
+ Message string `json:"message"`
138
+ Type string `json:"type"`
139
+ Code string `json:"code,omitempty"`
140
+ }
141
+
142
+ // CursorMessage Cursor消息格式
143
+ type CursorMessage struct {
144
+ Role string `json:"role"`
145
+ Parts []CursorPart `json:"parts"`
146
+ }
147
+
148
+ // CursorPart Cursor消息部分
149
+ type CursorPart struct {
150
+ Type string `json:"type"`
151
+ Text string `json:"text"`
152
+ }
153
+
154
+ // CursorRequest Cursor请求格式
155
+ type CursorRequest struct {
156
+ Context []interface{} `json:"context"`
157
+ Model string `json:"model"`
158
+ ID string `json:"id"`
159
+ Messages []CursorMessage `json:"messages"`
160
+ Trigger string `json:"trigger"`
161
+ }
162
+
163
+ // CursorEventData Cursor事件数据
164
+ type CursorEventData struct {
165
+ Type string `json:"type"`
166
+ Delta string `json:"delta,omitempty"`
167
+ ErrorText string `json:"errorText,omitempty"`
168
+ MessageMetadata *CursorMessageMetadata `json:"messageMetadata,omitempty"`
169
+ }
170
+
171
+ // CursorMessageMetadata Cursor消息元数据
172
+ type CursorMessageMetadata struct {
173
+ Usage *CursorUsage `json:"usage,omitempty"`
174
+ }
175
+
176
+ // CursorUsage Cursor使用统计
177
+ type CursorUsage struct {
178
+ InputTokens int `json:"inputTokens"`
179
+ OutputTokens int `json:"outputTokens"`
180
+ TotalTokens int `json:"totalTokens"`
181
+ }
182
+
183
+ // SSEEvent 服务器发送事件
184
+ type SSEEvent struct {
185
+ Data string `json:"data"`
186
+ Event string `json:"event,omitempty"`
187
+ ID string `json:"id,omitempty"`
188
+ }
189
+
190
+ // GetStringContent 获取消息的字符串内容
191
+ func (m *Message) GetStringContent() string {
192
+ if m.Content == nil {
193
+ return ""
194
+ }
195
+
196
+ switch content := m.Content.(type) {
197
+ case string:
198
+ return content
199
+ case []ContentPart:
200
+ var text string
201
+ for _, part := range content {
202
+ if part.Type == "text" {
203
+ text += part.Text
204
+ }
205
+ }
206
+ return text
207
+ case []interface{}:
208
+ // 处理混合类型内容
209
+ var text string
210
+ for _, item := range content {
211
+ if part, ok := item.(map[string]interface{}); ok {
212
+ if partType, exists := part["type"].(string); exists && partType == "text" {
213
+ if textContent, exists := part["text"].(string); exists {
214
+ text += textContent
215
+ }
216
+ }
217
+ }
218
+ }
219
+ return text
220
+ default:
221
+ // 尝试将其他类型转换为JSON字符串
222
+ if data, err := json.Marshal(content); err == nil {
223
+ return string(data)
224
+ }
225
+ return ""
226
+ }
227
+ }
228
+
229
+ // ToCursorMessages 将OpenAI消息转换为Cursor格式
230
+ func ToCursorMessages(messages []Message, systemPromptInject string) []CursorMessage {
231
+ var result []CursorMessage
232
+
233
+ // 处理系统提示注入
234
+ if systemPromptInject != "" {
235
+ if len(messages) > 0 && messages[0].Role == "system" {
236
+ // 如果第一条已经是系统消息,追加注入内容
237
+ content := messages[0].GetStringContent()
238
+ content += "\n" + systemPromptInject
239
+ result = append(result, CursorMessage{
240
+ Role: "system",
241
+ Parts: []CursorPart{
242
+ {Type: "text", Text: content},
243
+ },
244
+ })
245
+ messages = messages[1:] // 跳过第一条消息
246
+ } else {
247
+ // 如果第一条不是系统消息或没有消息,插入新的系统消息
248
+ result = append(result, CursorMessage{
249
+ Role: "system",
250
+ Parts: []CursorPart{
251
+ {Type: "text", Text: systemPromptInject},
252
+ },
253
+ })
254
+ }
255
+ } else if len(messages) > 0 && messages[0].Role == "system" {
256
+ // 如果有系统消息但没有注入内容,直接添加
257
+ result = append(result, CursorMessage{
258
+ Role: "system",
259
+ Parts: []CursorPart{
260
+ {Type: "text", Text: messages[0].GetStringContent()},
261
+ },
262
+ })
263
+ messages = messages[1:] // 跳过第一条消息
264
+ }
265
+
266
+ // 转换其余消息
267
+ for _, msg := range messages {
268
+ if msg.Role == "" {
269
+ continue // 跳过空消息
270
+ }
271
+
272
+ cursorMsg := CursorMessage{
273
+ Role: msg.Role,
274
+ Parts: []CursorPart{
275
+ {
276
+ Type: "text",
277
+ Text: msg.GetStringContent(),
278
+ },
279
+ },
280
+ }
281
+ result = append(result, cursorMsg)
282
+ }
283
+
284
+ return result
285
+ }
286
+
287
+ // NewChatCompletionResponse 创建聊天完成响应
288
+ func NewChatCompletionResponse(id, model, content string, usage Usage) *ChatCompletionResponse {
289
+ return &ChatCompletionResponse{
290
+ ID: id,
291
+ Object: "chat.completion",
292
+ Created: time.Now().Unix(),
293
+ Model: model,
294
+ Choices: []Choice{
295
+ {
296
+ Index: 0,
297
+ Message: Message{
298
+ Role: "assistant",
299
+ Content: content,
300
+ },
301
+ FinishReason: "stop",
302
+ },
303
+ },
304
+ Usage: usage,
305
+ }
306
+ }
307
+
308
+ // NewChatCompletionStreamResponse 创建流式响应
309
+ func NewChatCompletionStreamResponse(id, model, content string, finishReason *string) *ChatCompletionStreamResponse {
310
+ return &ChatCompletionStreamResponse{
311
+ ID: id,
312
+ Object: "chat.completion.chunk",
313
+ Created: time.Now().Unix(),
314
+ Model: model,
315
+ Choices: []StreamChoice{
316
+ {
317
+ Index: 0,
318
+ Delta: StreamDelta{
319
+ Content: content,
320
+ },
321
+ FinishReason: finishReason,
322
+ },
323
+ },
324
+ }
325
+ }
326
+
327
+ // NewErrorResponse 创建错误响应
328
+ func NewErrorResponse(message, errorType, code string) *ErrorResponse {
329
+ return &ErrorResponse{
330
+ Error: ErrorDetail{
331
+ Message: message,
332
+ Type: errorType,
333
+ Code: code,
334
+ },
335
+ }
336
+ }
models/models_test.go ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package models
22
+
23
+ import (
24
+ "testing"
25
+ )
26
+
27
+ func TestGetStringContent(t *testing.T) {
28
+ tests := []struct {
29
+ name string
30
+ content interface{}
31
+ expected string
32
+ }{
33
+ {
34
+ name: "string content",
35
+ content: "Hello world",
36
+ expected: "Hello world",
37
+ },
38
+ {
39
+ name: "array content",
40
+ content: []ContentPart{
41
+ {Type: "text", Text: "Hello"},
42
+ {Type: "text", Text: " world"},
43
+ },
44
+ expected: "Hello world",
45
+ },
46
+ {
47
+ name: "empty array",
48
+ content: []ContentPart{},
49
+ expected: "",
50
+ },
51
+ {
52
+ name: "nil content",
53
+ content: nil,
54
+ expected: "",
55
+ },
56
+ }
57
+
58
+ for _, tt := range tests {
59
+ t.Run(tt.name, func(t *testing.T) {
60
+ msg := &Message{Content: tt.content}
61
+ result := msg.GetStringContent()
62
+ if result != tt.expected {
63
+ t.Errorf("GetStringContent() = %v, want %v", result, tt.expected)
64
+ }
65
+ })
66
+ }
67
+ }
68
+
69
+ func TestToCursorMessages(t *testing.T) {
70
+ tests := []struct {
71
+ name string
72
+ messages []Message
73
+ systemPrompt string
74
+ expectedLength int
75
+ expectedFirstMsg string
76
+ }{
77
+ {
78
+ name: "no system prompt",
79
+ messages: []Message{
80
+ {Role: "user", Content: "Hello"},
81
+ },
82
+ systemPrompt: "",
83
+ expectedLength: 1,
84
+ expectedFirstMsg: "Hello",
85
+ },
86
+ {
87
+ name: "with system prompt, no system message",
88
+ messages: []Message{
89
+ {Role: "user", Content: "Hello"},
90
+ },
91
+ systemPrompt: "You are a helpful assistant",
92
+ expectedLength: 2,
93
+ expectedFirstMsg: "You are a helpful assistant",
94
+ },
95
+ {
96
+ name: "with system prompt, has system message",
97
+ messages: []Message{
98
+ {Role: "system", Content: "Be helpful"},
99
+ {Role: "user", Content: "Hello"},
100
+ },
101
+ systemPrompt: "You are an AI",
102
+ expectedLength: 2,
103
+ expectedFirstMsg: "Be helpful\nYou are an AI",
104
+ },
105
+ }
106
+
107
+ for _, tt := range tests {
108
+ t.Run(tt.name, func(t *testing.T) {
109
+ result := ToCursorMessages(tt.messages, tt.systemPrompt)
110
+ if len(result) != tt.expectedLength {
111
+ t.Errorf("ToCursorMessages() length = %v, want %v", len(result), tt.expectedLength)
112
+ }
113
+ if len(result) > 0 && result[0].Parts[0].Text != tt.expectedFirstMsg {
114
+ t.Errorf("ToCursorMessages() first message = %v, want %v", result[0].Parts[0].Text, tt.expectedFirstMsg)
115
+ }
116
+ })
117
+ }
118
+ }
119
+
120
+ func TestNewChatCompletionResponse(t *testing.T) {
121
+ response := NewChatCompletionResponse("test-id", "gpt-4o", "Hello world", Usage{PromptTokens: 10, CompletionTokens: 5, TotalTokens: 15})
122
+
123
+ if response.ID != "test-id" {
124
+ t.Errorf("ID = %v, want test-id", response.ID)
125
+ }
126
+ if response.Model != "gpt-4o" {
127
+ t.Errorf("Model = %v, want gpt-4o", response.Model)
128
+ }
129
+ if response.Choices[0].Message.Content != "Hello world" {
130
+ t.Errorf("Content = %v, want Hello world", response.Choices[0].Message.Content)
131
+ }
132
+ if response.Usage.PromptTokens != 10 {
133
+ t.Errorf("PromptTokens = %v, want 10", response.Usage.PromptTokens)
134
+ }
135
+ }
136
+
137
+ func TestNewChatCompletionStreamResponse(t *testing.T) {
138
+ response := NewChatCompletionStreamResponse("test-id", "gpt-4o", "Hello", stringPtr("stop"))
139
+
140
+ if response.ID != "test-id" {
141
+ t.Errorf("ID = %v, want test-id", response.ID)
142
+ }
143
+ if response.Choices[0].Delta.Content != "Hello" {
144
+ t.Errorf("Content = %v, want Hello", response.Choices[0].Delta.Content)
145
+ }
146
+ if response.Choices[0].FinishReason == nil || *response.Choices[0].FinishReason != "stop" {
147
+ t.Errorf("FinishReason = %v, want stop", response.Choices[0].FinishReason)
148
+ }
149
+ }
150
+
151
+ func TestNewErrorResponse(t *testing.T) {
152
+ response := NewErrorResponse("Test error", "test_error", "error_code")
153
+
154
+ if response.Error.Message != "Test error" {
155
+ t.Errorf("Message = %v, want Test error", response.Error.Message)
156
+ }
157
+ if response.Error.Type != "test_error" {
158
+ t.Errorf("Type = %v, want test_error", response.Error.Type)
159
+ }
160
+ if response.Error.Code != "error_code" {
161
+ t.Errorf("Code = %v, want error_code", response.Error.Code)
162
+ }
163
+ }
164
+
165
+ // Helper function
166
+ func stringPtr(s string) *string {
167
+ return &s
168
+ }
services/cursor.go ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package services
22
+
23
+ import (
24
+ "context"
25
+ "cursor2api-go/config"
26
+ "cursor2api-go/middleware"
27
+ "cursor2api-go/models"
28
+ "cursor2api-go/utils"
29
+ "encoding/json"
30
+ "errors"
31
+ "fmt"
32
+ "io"
33
+ "net/http"
34
+ "net/http/cookiejar"
35
+ "os"
36
+ "path/filepath"
37
+ "strings"
38
+ "sync"
39
+ "time"
40
+
41
+ "github.com/imroc/req/v3"
42
+ "github.com/sirupsen/logrus"
43
+ )
44
+
45
+ const cursorAPIURL = "https://cursor.com/api/chat"
46
+
47
+ // CursorService handles interactions with Cursor API.
48
+ type CursorService struct {
49
+ config *config.Config
50
+ client *req.Client
51
+ mainJS string
52
+ envJS string
53
+ scriptCache string
54
+ scriptCacheTime time.Time
55
+ scriptMutex sync.RWMutex
56
+ headerGenerator *utils.HeaderGenerator
57
+ }
58
+
59
+ // NewCursorService creates a new service instance.
60
+ func NewCursorService(cfg *config.Config) *CursorService {
61
+ mainJS, err := os.ReadFile(filepath.Join("jscode", "main.js"))
62
+ if err != nil {
63
+ logrus.Fatalf("failed to read jscode/main.js: %v", err)
64
+ }
65
+
66
+ envJS, err := os.ReadFile(filepath.Join("jscode", "env.js"))
67
+ if err != nil {
68
+ logrus.Fatalf("failed to read jscode/env.js: %v", err)
69
+ }
70
+
71
+ jar, err := cookiejar.New(nil)
72
+ if err != nil {
73
+ logrus.Warnf("failed to create cookie jar: %v", err)
74
+ }
75
+
76
+ client := req.C()
77
+ client.SetTimeout(time.Duration(cfg.Timeout) * time.Second)
78
+ client.ImpersonateChrome()
79
+ if jar != nil {
80
+ client.SetCookieJar(jar)
81
+ }
82
+
83
+ return &CursorService{
84
+ config: cfg,
85
+ client: client,
86
+ mainJS: string(mainJS),
87
+ envJS: string(envJS),
88
+ headerGenerator: utils.NewHeaderGenerator(),
89
+ }
90
+ }
91
+
92
+ // ChatCompletion creates a chat completion stream for the given request.
93
+ func (s *CursorService) ChatCompletion(ctx context.Context, request *models.ChatCompletionRequest) (<-chan interface{}, error) {
94
+ truncatedMessages := s.truncateMessages(request.Messages)
95
+ cursorMessages := models.ToCursorMessages(truncatedMessages, s.config.SystemPromptInject)
96
+
97
+ // 获取Cursor API使用的实际模型名称
98
+ cursorModel := models.GetCursorModel(request.Model)
99
+
100
+ payload := models.CursorRequest{
101
+ Context: []interface{}{},
102
+ Model: cursorModel,
103
+ ID: utils.GenerateRandomString(16),
104
+ Messages: cursorMessages,
105
+ Trigger: "submit-message",
106
+ }
107
+
108
+ jsonPayload, err := json.Marshal(payload)
109
+ if err != nil {
110
+ return nil, fmt.Errorf("failed to marshal cursor payload: %w", err)
111
+ }
112
+
113
+ // 尝试最多2次
114
+ maxRetries := 2
115
+ for attempt := 1; attempt <= maxRetries; attempt++ {
116
+ xIsHuman, err := s.fetchXIsHuman(ctx)
117
+ if err != nil {
118
+ if attempt < maxRetries {
119
+ logrus.WithError(err).Warnf("Failed to fetch x-is-human token (attempt %d/%d), retrying...", attempt, maxRetries)
120
+ time.Sleep(time.Second * time.Duration(attempt)) // 指数退避
121
+ continue
122
+ }
123
+ return nil, err
124
+ }
125
+
126
+ // 添加详细的调试日志
127
+ headers := s.chatHeaders(xIsHuman)
128
+ logrus.WithFields(logrus.Fields{
129
+ "url": cursorAPIURL,
130
+ "x-is-human": xIsHuman[:50] + "...", // 只显示前50个字符
131
+ "payload_length": len(jsonPayload),
132
+ "model": request.Model,
133
+ "attempt": attempt,
134
+ }).Debug("Sending request to Cursor API")
135
+
136
+ resp, err := s.client.R().
137
+ SetContext(ctx).
138
+ SetHeaders(headers).
139
+ SetBody(jsonPayload).
140
+ DisableAutoReadResponse().
141
+ Post(cursorAPIURL)
142
+ if err != nil {
143
+ if attempt < maxRetries {
144
+ logrus.WithError(err).Warnf("Cursor request failed (attempt %d/%d), retrying...", attempt, maxRetries)
145
+ time.Sleep(time.Second * time.Duration(attempt))
146
+ continue
147
+ }
148
+ return nil, fmt.Errorf("cursor request failed: %w", err)
149
+ }
150
+
151
+ if resp.StatusCode != http.StatusOK {
152
+ body, _ := io.ReadAll(resp.Response.Body)
153
+ resp.Response.Body.Close()
154
+ message := strings.TrimSpace(string(body))
155
+
156
+ // 记录详细的错误信息
157
+ logrus.WithFields(logrus.Fields{
158
+ "status_code": resp.StatusCode,
159
+ "response": message,
160
+ "headers": resp.Header,
161
+ "attempt": attempt,
162
+ }).Error("Cursor API returned non-OK status")
163
+
164
+ // 如果是 403 错误且还有重试机会,清除缓存并重试
165
+ if resp.StatusCode == http.StatusForbidden && attempt < maxRetries {
166
+ logrus.Warn("Received 403 Access Denied, refreshing browser fingerprint and clearing token cache...")
167
+
168
+ // 刷新浏览器指纹
169
+ s.headerGenerator.Refresh()
170
+ logrus.WithFields(logrus.Fields{
171
+ "platform": s.headerGenerator.GetProfile().Platform,
172
+ "chrome_version": s.headerGenerator.GetProfile().ChromeVersion,
173
+ }).Debug("Refreshed browser fingerprint")
174
+
175
+ // 清除 token 缓存
176
+ s.scriptMutex.Lock()
177
+ s.scriptCache = ""
178
+ s.scriptCacheTime = time.Time{}
179
+ s.scriptMutex.Unlock()
180
+
181
+ time.Sleep(time.Second * time.Duration(attempt))
182
+ continue
183
+ }
184
+
185
+ if strings.Contains(message, "Attention Required! | Cloudflare") {
186
+ message = "Cloudflare 403"
187
+ }
188
+ return nil, middleware.NewCursorWebError(resp.StatusCode, message)
189
+ }
190
+
191
+ // 成功,返回结果
192
+ output := make(chan interface{}, 32)
193
+ go s.consumeSSE(ctx, resp.Response, output)
194
+ return output, nil
195
+ }
196
+
197
+ return nil, fmt.Errorf("failed after %d attempts", maxRetries)
198
+ }
199
+
200
+ func (s *CursorService) consumeSSE(ctx context.Context, resp *http.Response, output chan interface{}) {
201
+ defer close(output)
202
+
203
+ if err := utils.ReadSSEStream(ctx, resp, output); err != nil {
204
+ if errors.Is(err, context.Canceled) {
205
+ return
206
+ }
207
+ errResp := middleware.NewCursorWebError(http.StatusBadGateway, err.Error())
208
+ select {
209
+ case output <- errResp:
210
+ default:
211
+ logrus.WithError(err).Warn("failed to push SSE error to channel")
212
+ }
213
+ }
214
+ }
215
+
216
+ func (s *CursorService) fetchXIsHuman(ctx context.Context) (string, error) {
217
+ // 检查缓存
218
+ s.scriptMutex.RLock()
219
+ cached := s.scriptCache
220
+ lastFetch := s.scriptCacheTime
221
+ s.scriptMutex.RUnlock()
222
+
223
+ var scriptBody string
224
+ // 缓存有效期缩短到1分钟,避免 token 过期
225
+ if cached != "" && time.Since(lastFetch) < 1*time.Minute {
226
+ scriptBody = cached
227
+ } else {
228
+ resp, err := s.client.R().
229
+ SetContext(ctx).
230
+ SetHeaders(s.scriptHeaders()).
231
+ Get(s.config.ScriptURL)
232
+
233
+ if err != nil {
234
+ // 如果请求失败且有缓存,使用缓存
235
+ if cached != "" {
236
+ logrus.Warnf("Failed to fetch script, using cached version: %v", err)
237
+ scriptBody = cached
238
+ } else {
239
+ // 清除缓存并生成一个简单的token
240
+ s.scriptMutex.Lock()
241
+ s.scriptCache = ""
242
+ s.scriptCacheTime = time.Time{}
243
+ s.scriptMutex.Unlock()
244
+ // 生成一个简单的x-is-human token作为fallback
245
+ token := utils.GenerateRandomString(64)
246
+ logrus.Warnf("Failed to fetch script, generated fallback token")
247
+ return token, nil
248
+ }
249
+ } else if resp.StatusCode != http.StatusOK {
250
+ // 如果状态码异常且有缓存,使用缓存
251
+ if cached != "" {
252
+ logrus.Warnf("Script fetch returned status %d, using cached version", resp.StatusCode)
253
+ scriptBody = cached
254
+ } else {
255
+ // 清除缓存并生成一个简单的token
256
+ s.scriptMutex.Lock()
257
+ s.scriptCache = ""
258
+ s.scriptCacheTime = time.Time{}
259
+ s.scriptMutex.Unlock()
260
+ // 生成一个简单的x-is-human token作为fallback
261
+ token := utils.GenerateRandomString(64)
262
+ logrus.Warnf("Script fetch returned status %d, generated fallback token", resp.StatusCode)
263
+ return token, nil
264
+ }
265
+ } else {
266
+ scriptBody = string(resp.Bytes())
267
+ // 更新缓存
268
+ s.scriptMutex.Lock()
269
+ s.scriptCache = scriptBody
270
+ s.scriptCacheTime = time.Now()
271
+ s.scriptMutex.Unlock()
272
+ }
273
+ }
274
+
275
+ compiled := s.prepareJS(scriptBody)
276
+ value, err := utils.RunJS(compiled)
277
+ if err != nil {
278
+ // JS 执行失败时清除缓存并生成fallback token
279
+ s.scriptMutex.Lock()
280
+ s.scriptCache = ""
281
+ s.scriptCacheTime = time.Time{}
282
+ s.scriptMutex.Unlock()
283
+ token := utils.GenerateRandomString(64)
284
+ logrus.Warnf("Failed to execute JS, generated fallback token: %v", err)
285
+ return token, nil
286
+ }
287
+
288
+ logrus.WithField("length", len(value)).Debug("Fetched x-is-human token")
289
+
290
+ return value, nil
291
+ }
292
+
293
+ func (s *CursorService) prepareJS(cursorJS string) string {
294
+ replacer := strings.NewReplacer(
295
+ "$$currentScriptSrc$$", s.config.ScriptURL,
296
+ "$$UNMASKED_VENDOR_WEBGL$$", s.config.FP.UNMASKED_VENDOR_WEBGL,
297
+ "$$UNMASKED_RENDERER_WEBGL$$", s.config.FP.UNMASKED_RENDERER_WEBGL,
298
+ "$$userAgent$$", s.config.FP.UserAgent,
299
+ )
300
+
301
+ mainScript := replacer.Replace(s.mainJS)
302
+ mainScript = strings.Replace(mainScript, "$$env_jscode$$", s.envJS, 1)
303
+ mainScript = strings.Replace(mainScript, "$$cursor_jscode$$", cursorJS, 1)
304
+ return mainScript
305
+ }
306
+
307
+ func (s *CursorService) truncateMessages(messages []models.Message) []models.Message {
308
+ if len(messages) == 0 || s.config.MaxInputLength <= 0 {
309
+ return messages
310
+ }
311
+
312
+ maxLength := s.config.MaxInputLength
313
+ total := 0
314
+ for _, msg := range messages {
315
+ total += len(msg.GetStringContent())
316
+ }
317
+
318
+ if total <= maxLength {
319
+ return messages
320
+ }
321
+
322
+ var result []models.Message
323
+ startIdx := 0
324
+
325
+ if strings.EqualFold(messages[0].Role, "system") {
326
+ result = append(result, messages[0])
327
+ maxLength -= len(messages[0].GetStringContent())
328
+ if maxLength < 0 {
329
+ maxLength = 0
330
+ }
331
+ startIdx = 1
332
+ }
333
+
334
+ current := 0
335
+ collected := make([]models.Message, 0, len(messages)-startIdx)
336
+ for i := len(messages) - 1; i >= startIdx; i-- {
337
+ msg := messages[i]
338
+ msgLen := len(msg.GetStringContent())
339
+ if msgLen == 0 {
340
+ continue
341
+ }
342
+ if current+msgLen > maxLength {
343
+ continue
344
+ }
345
+ collected = append(collected, msg)
346
+ current += msgLen
347
+ }
348
+
349
+ for i, j := 0, len(collected)-1; i < j; i, j = i+1, j-1 {
350
+ collected[i], collected[j] = collected[j], collected[i]
351
+ }
352
+
353
+ return append(result, collected...)
354
+ }
355
+
356
+ func (s *CursorService) chatHeaders(xIsHuman string) map[string]string {
357
+ return s.headerGenerator.GetChatHeaders(xIsHuman)
358
+ }
359
+
360
+ func (s *CursorService) scriptHeaders() map[string]string {
361
+ return s.headerGenerator.GetScriptHeaders()
362
+ }
start-go-utf8.bat ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ chcp 65001 >nul 2>&1
3
+ setlocal enabledelayedexpansion
4
+
5
+ :: Cursor2API启动脚本
6
+
7
+ echo.
8
+ echo =========================================
9
+ echo 🚀 Cursor2API启动器
10
+ echo =========================================
11
+ echo.
12
+
13
+ :: 检查Go是否安装
14
+ go version >nul 2>&1
15
+ if errorlevel 1 (
16
+ echo ❌ Go 未安装,请先安装 Go 1.21 或更高版本
17
+ echo 💡 安装方法: https://golang.org/dl/
18
+ pause
19
+ exit /b 1
20
+ )
21
+
22
+ :: 显示Go版本并检查版本号
23
+ for /f "tokens=3" %%i in ('go version') do set GO_VERSION=%%i
24
+ set GO_VERSION=!GO_VERSION:go=!
25
+
26
+ :: 检查Go版本是否满足要求 (需要 >= 1.21)
27
+ for /f "tokens=1,2 delims=." %%a in ("!GO_VERSION!") do (
28
+ set MAJOR=%%a
29
+ set MINOR=%%b
30
+ )
31
+ if !MAJOR! LSS 1 (
32
+ echo ❌ Go 版本 !GO_VERSION! 过低,请安装 Go 1.21 或更高版本
33
+ pause
34
+ exit /b 1
35
+ )
36
+ if !MAJOR! EQU 1 if !MINOR! LSS 21 (
37
+ echo ❌ Go 版本 !GO_VERSION! 过低,请安装 Go 1.21 或更高版本
38
+ pause
39
+ exit /b 1
40
+ )
41
+
42
+ echo ✅ Go 版本检查通过: !GO_VERSION!
43
+
44
+ :: 检查Node.js是否安装
45
+ node --version >nul 2>&1
46
+ if errorlevel 1 (
47
+ echo ❌ Node.js 未安装,请先安装 Node.js 18 或更高版本
48
+ echo 💡 安装方法: https://nodejs.org/
49
+ pause
50
+ exit /b 1
51
+ )
52
+
53
+ :: 显示Node.js版本并检查版本号
54
+ for /f "delims=" %%i in ('node --version') do set NODE_VERSION=%%i
55
+ set NODE_VERSION=!NODE_VERSION:v=!
56
+
57
+ :: 检查Node.js版本是否满足要求 (需要 >= 18)
58
+ for /f "tokens=1 delims=." %%a in ("!NODE_VERSION!") do set NODE_MAJOR=%%a
59
+ if !NODE_MAJOR! LSS 18 (
60
+ echo ❌ Node.js 版本 !NODE_VERSION! 过低,请安装 Node.js 18 或更高版本
61
+ pause
62
+ exit /b 1
63
+ )
64
+
65
+ echo ✅ Node.js 版本检查通过: !NODE_VERSION!
66
+
67
+ :: 创建.env文件(如果不存在)
68
+ if not exist .env (
69
+ echo 📝 创建默认 .env 配置文件...
70
+ (
71
+ echo # 服务器配置
72
+ echo PORT=8002
73
+ echo DEBUG=false
74
+ echo.
75
+ echo # API配置
76
+ echo API_KEY=0000
77
+ echo MODELS=gpt-5.1,gpt-5,gpt-5-codex,gpt-5-mini,gpt-5-nano,gpt-4.1,gpt-4o,claude-3.5-sonnet,claude-3.5-haiku,claude-3.7-sonnet,claude-4-sonnet,claude-4.5-sonnet,claude-4-opus,claude-4.1-opus,gemini-2.5-pro,gemini-2.5-flash,gemini-3.0-pro,o3,o4-mini,deepseek-r1,deepseek-v3.1,kimi-k2-instruct,grok-3
78
+ echo SYSTEM_PROMPT_INJECT=
79
+ echo.
80
+ echo # 请求配置
81
+ echo TIMEOUT=30
82
+ echo USER_AGENT=Mozilla/5.0 ^(Windows NT 10.0; Win64; x64^) AppleWebKit/537.36 ^(KHTML, like Gecko^) Chrome/140.0.0.0 Safari/537.36
83
+ echo.
84
+ echo # Cursor配置
85
+ echo SCRIPT_URL=https://cursor.com/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/a-4-a/c.js?i=0^^^&v=3^^^&h=cursor.com
86
+ ) > .env
87
+ echo ✅ 默认 .env 文件已创建
88
+ ) else (
89
+ echo ✅ 配置文件 .env 已存在
90
+ )
91
+
92
+ :: 下载依赖
93
+ echo.
94
+ echo 📦 正在下载 Go 依赖...
95
+ go mod download
96
+ if errorlevel 1 (
97
+ echo ❌ 依赖下载失败!
98
+ pause
99
+ exit /b 1
100
+ )
101
+
102
+ :: 构建应用
103
+ echo 🔨 正在编译 Go 应用...
104
+ go build -o cursor2api-go.exe .
105
+ if errorlevel 1 (
106
+ echo ❌ 编译失败!
107
+ pause
108
+ exit /b 1
109
+ )
110
+
111
+ :: 检查构建是否成功
112
+ if not exist cursor2api-go.exe (
113
+ echo ❌ 编译失败 - 可执行文件未找到!
114
+ pause
115
+ exit /b 1
116
+ )
117
+
118
+ echo ✅ 应用编译成功!
119
+
120
+ :: 显示服务信息
121
+ echo.
122
+ echo ✅ 准备就绪,正在启动服务...
123
+ echo.
124
+
125
+ :: 启动服务
126
+ cursor2api-go.exe
127
+
128
+ pause
start-go.bat ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ chcp 65001 >nul 2>&1
3
+ setlocal enabledelayedexpansion
4
+
5
+ :: Cursor2API Go启动脚本
6
+
7
+ echo.
8
+ echo =========================================
9
+ echo 🚀 Cursor2API启动器 Go版本
10
+ echo =========================================
11
+ echo.
12
+
13
+ :: 检查Go是否安装
14
+ go version >nul 2>&1
15
+ if errorlevel 1 (
16
+ echo [错误] Go 未安装,请先安装 Go 1.21 或更高版本
17
+ echo [提示] 安装方法: https://golang.org/dl/
18
+ pause
19
+ exit /b 1
20
+ )
21
+
22
+ :: 显示Go版本并检查版本号
23
+ for /f "tokens=3" %%i in ('go version') do set GO_VERSION=%%i
24
+ set GO_VERSION=!GO_VERSION:go=!
25
+
26
+ :: 检查Go版本是否满足要求 (需要 >= 1.21)
27
+ for /f "tokens=1,2 delims=." %%a in ("!GO_VERSION!") do (
28
+ set MAJOR=%%a
29
+ set MINOR=%%b
30
+ )
31
+ if !MAJOR! LSS 1 (
32
+ echo [错误] Go 版本 !GO_VERSION! 过低,请安装 Go 1.21 或更高版本
33
+ pause
34
+ exit /b 1
35
+ )
36
+ if !MAJOR! EQU 1 if !MINOR! LSS 21 (
37
+ echo [错误] Go 版本 !GO_VERSION! 过低,请安装 Go 1.21 或更高版本
38
+ pause
39
+ exit /b 1
40
+ )
41
+
42
+ echo [成功] Go 版本检查通过: !GO_VERSION!
43
+
44
+ :: 检查Node.js是否安装
45
+ node --version >nul 2>&1
46
+ if errorlevel 1 (
47
+ echo [错误] Node.js 未安装,请先安装 Node.js 18 或更高版本
48
+ echo [提示] 安装方法: https://nodejs.org/
49
+ pause
50
+ exit /b 1
51
+ )
52
+
53
+ :: 显示Node.js版本并检查版本号
54
+ for /f "delims=" %%i in ('node --version') do set NODE_VERSION=%%i
55
+ set NODE_VERSION=!NODE_VERSION:v=!
56
+
57
+ :: 检查Node.js版本是否满足要求 (需要 >= 18)
58
+ for /f "tokens=1 delims=." %%a in ("!NODE_VERSION!") do set NODE_MAJOR=%%a
59
+ if !NODE_MAJOR! LSS 18 (
60
+ echo [错误] Node.js 版本 !NODE_VERSION! 过低,请安装 Node.js 18 或更高版本
61
+ pause
62
+ exit /b 1
63
+ )
64
+
65
+ echo [成功] Node.js 版本检查通过: !NODE_VERSION!
66
+
67
+ :: 创建.env文件(如果不存在)
68
+ if not exist .env (
69
+ echo [信息] 创建默认 .env 配置文件...
70
+ (
71
+ echo # 服务器配置
72
+ echo PORT=8002
73
+ echo DEBUG=false
74
+ echo.
75
+ echo # API配置
76
+ echo API_KEY=0000
77
+ echo MODELS=gpt-5.1,gpt-5,gpt-5-codex,gpt-5-mini,gpt-5-nano,gpt-4.1,gpt-4o,claude-3.5-sonnet,claude-3.5-haiku,claude-3.7-sonnet,claude-4-sonnet,claude-4.5-sonnet,claude-4-opus,claude-4.1-opus,gemini-2.5-pro,gemini-2.5-flash,gemini-3.0-pro,o3,o4-mini,deepseek-r1,deepseek-v3.1,kimi-k2-instruct,grok-3
78
+ echo SYSTEM_PROMPT_INJECT=
79
+ echo.
80
+ echo # 请求配置
81
+ echo TIMEOUT=30
82
+ echo USER_AGENT=Mozilla/5.0 ^(Windows NT 10.0; Win64; x64^) AppleWebKit/537.36 ^(KHTML, like Gecko^) Chrome/140.0.0.0 Safari/537.36
83
+ echo.
84
+ echo # Cursor配置
85
+ echo SCRIPT_URL=https://cursor.com/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/a-4-a/c.js?i=0^^^&v=3^^^&h=cursor.com
86
+ ) > .env
87
+ echo [成功] 默认 .env 文件已创建
88
+ ) else (
89
+ echo [成功] 配置文件 .env 已存在
90
+ )
91
+
92
+ :: 下载依赖
93
+ echo.
94
+ echo [信息] 正在下载 Go 依赖...
95
+ go mod download
96
+ if errorlevel 1 (
97
+ echo [错误] 依赖下载失败!
98
+ pause
99
+ exit /b 1
100
+ )
101
+
102
+ :: 构建应用
103
+ echo [信息] 正在编译 Go 应用...
104
+ go build -o cursor2api-go.exe .
105
+ if errorlevel 1 (
106
+ echo [错误] 编译失败!
107
+ pause
108
+ exit /b 1
109
+ )
110
+
111
+ :: 检查构建是否成功
112
+ if not exist cursor2api-go.exe (
113
+ echo [错误] 编译失败 - 可执行文件未找到
114
+ pause
115
+ exit /b 1
116
+ )
117
+
118
+ echo [成功] 应用编译成功!
119
+
120
+ :: 显示服务信息
121
+ echo.
122
+ echo [成功] 准备就绪,正在启动服务...
123
+ echo.
124
+
125
+ :: 启动服务
126
+ cursor2api-go.exe
127
+
128
+ pause
start.sh ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Cursor2API启动脚本
4
+
5
+ set -e
6
+
7
+ # 定义颜色代码
8
+ RED='\033[0;31m'
9
+ GREEN='\033[0;32m'
10
+ BLUE='\033[0;34m'
11
+ YELLOW='\033[1;33m'
12
+ PURPLE='\033[0;35m'
13
+ CYAN='\033[0;36m'
14
+ WHITE='\033[1;37m'
15
+ NC='\033[0m' # No Color
16
+
17
+ # 打印标题
18
+ print_header() {
19
+ echo ""
20
+ echo -e "${CYAN}=========================================${NC}"
21
+ echo -e "${WHITE} 🚀 Cursor2API启动器${NC}"
22
+ echo -e "${CYAN}=========================================${NC}"
23
+ }
24
+
25
+ # 检查Go环境
26
+ check_go() {
27
+ if ! command -v go &> /dev/null; then
28
+ echo -e "${RED}❌ Go 未安装,请先安装 Go 1.21 或更高版本${NC}"
29
+ echo -e "${YELLOW}💡 安装方法: https://golang.org/dl/${NC}"
30
+ exit 1
31
+ fi
32
+
33
+ GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
34
+ REQUIRED_VERSION="1.21"
35
+
36
+ if [ "$(printf '%s\n' "$REQUIRED_VERSION" "$GO_VERSION" | sort -V | head -n1)" != "$REQUIRED_VERSION" ]; then
37
+ echo -e "${RED}❌ Go 版本 $GO_VERSION 过低,请安装 Go $REQUIRED_VERSION 或更高版本${NC}"
38
+ exit 1
39
+ fi
40
+
41
+ echo -e "${GREEN}✅ Go 版本检查通过: $GO_VERSION${NC}"
42
+ }
43
+
44
+ # 检查Node.js环境
45
+ check_nodejs() {
46
+ if ! command -v node &> /dev/null; then
47
+ echo -e "${RED}❌ Node.js 未安装,请先安装 Node.js 18 或更高版本${NC}"
48
+ echo -e "${YELLOW}💡 安装方法: https://nodejs.org/${NC}"
49
+ exit 1
50
+ fi
51
+
52
+ NODE_VERSION=$(node --version | sed 's/v//')
53
+ REQUIRED_VERSION="18.0.0"
54
+
55
+ if [ "$(printf '%s\n' "$REQUIRED_VERSION" "$NODE_VERSION" | sort -V | head -n1)" != "$REQUIRED_VERSION" ]; then
56
+ echo -e "${RED}❌ Node.js 版本 $NODE_VERSION 过低,请安装 Node.js $REQUIRED_VERSION 或更高版本${NC}"
57
+ exit 1
58
+ fi
59
+
60
+ echo -e "${GREEN}✅ Node.js 版本检查通过: $NODE_VERSION${NC}"
61
+ }
62
+
63
+ # 处理环境配置
64
+ setup_env() {
65
+ if [ ! -f .env ]; then
66
+ echo -e "${YELLOW}📝 创建默认 .env 配置文件...${NC}"
67
+ cat > .env << EOF
68
+ # 服务器配置
69
+ PORT=8002
70
+ DEBUG=false
71
+
72
+ # API配置
73
+ API_KEY=0000
74
+ MODELS=gpt-5.1,gpt-5,gpt-5-codex,gpt-5-mini,gpt-5-nano,gpt-4.1,gpt-4o,claude-3.5-sonnet,claude-3.5-haiku,claude-3.7-sonnet,claude-4-sonnet,claude-4.5-sonnet,claude-4-opus,claude-4.1-opus,gemini-2.5-pro,gemini-2.5-flash,gemini-3.0-pro,o3,o4-mini,deepseek-r1,deepseek-v3.1,kimi-k2-instruct,grok-3
75
+ SYSTEM_PROMPT_INJECT=
76
+
77
+ # 请求配置
78
+ TIMEOUT=30
79
+ USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
80
+
81
+ # Cursor配置
82
+ SCRIPT_URL=https://cursor.com/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/a-4-a/c.js?i=0&v=3&h=cursor.com
83
+ EOF
84
+ echo -e "${GREEN}✅ 默认 .env 文件已创建${NC}"
85
+ else
86
+ echo -e "${GREEN}✅ 配置文件 .env 已存在${NC}"
87
+ fi
88
+ }
89
+
90
+ # 构建应用
91
+ build_app() {
92
+ echo -e "${BLUE}📦 正在下载 Go 依赖...${NC}"
93
+ go mod download
94
+
95
+ echo -e "${BLUE}🔨 正在编译 Go 应用...${NC}"
96
+ go build -o cursor2api-go .
97
+
98
+ if [ ! -f cursor2api-go ]; then
99
+ echo -e "${RED}❌ 编译失败!${NC}"
100
+ exit 1
101
+ fi
102
+
103
+ echo -e "${GREEN}✅ 应用编译成功!${NC}"
104
+ }
105
+
106
+ # 显示服务信息
107
+ show_info() {
108
+ echo ""
109
+ echo -e "${GREEN}✅ 准备就绪,正在启动服务...${NC}"
110
+ echo ""
111
+ }
112
+
113
+ # 启动服务器
114
+ start_server() {
115
+ # 捕获中断信号
116
+ trap 'echo -e "\n${YELLOW}⏹️ 正在停止服务器...${NC}"; exit 0' INT
117
+
118
+ ./cursor2api-go
119
+ }
120
+
121
+ # 主函数
122
+ main() {
123
+ print_header
124
+ check_go
125
+ check_nodejs
126
+ setup_env
127
+ build_app
128
+ show_info
129
+ start_server
130
+ }
131
+
132
+ # 运行主函数
133
+ main
static/docs.html ADDED
@@ -0,0 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Cursor2API - 强大的AI模型API代理</title>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
17
+ line-height: 1.6;
18
+ color: #333;
19
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
20
+ min-height: 100vh;
21
+ }
22
+
23
+ .container {
24
+ max-width: 1200px;
25
+ margin: 0 auto;
26
+ padding: 20px;
27
+ }
28
+
29
+ .header {
30
+ text-align: center;
31
+ color: white;
32
+ margin-bottom: 40px;
33
+ }
34
+
35
+ .header h1 {
36
+ font-size: 3rem;
37
+ font-weight: 700;
38
+ margin-bottom: 10px;
39
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
40
+ }
41
+
42
+ .header .subtitle {
43
+ font-size: 1.2rem;
44
+ opacity: 0.9;
45
+ }
46
+
47
+ .main-content {
48
+ background: white;
49
+ border-radius: 20px;
50
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
51
+ overflow: hidden;
52
+ }
53
+
54
+ .nav-tabs {
55
+ display: flex;
56
+ background: #f8f9fa;
57
+ border-bottom: 1px solid #e9ecef;
58
+ }
59
+
60
+ .nav-tab {
61
+ flex: 1;
62
+ padding: 20px;
63
+ text-align: center;
64
+ cursor: pointer;
65
+ font-weight: 600;
66
+ color: #6c757d;
67
+ transition: all 0.3s ease;
68
+ border: none;
69
+ background: none;
70
+ }
71
+
72
+ .nav-tab:hover {
73
+ background: #e9ecef;
74
+ color: #495057;
75
+ }
76
+
77
+ .nav-tab.active {
78
+ background: #007bff;
79
+ color: white;
80
+ }
81
+
82
+ .tab-content {
83
+ display: none;
84
+ padding: 40px;
85
+ }
86
+
87
+ .tab-content.active {
88
+ display: block;
89
+ }
90
+
91
+ .status-card {
92
+ background: linear-gradient(135deg, #28a745, #20c997);
93
+ color: white;
94
+ padding: 30px;
95
+ border-radius: 15px;
96
+ margin-bottom: 30px;
97
+ text-align: center;
98
+ }
99
+
100
+ .api-info {
101
+ display: grid;
102
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
103
+ gap: 20px;
104
+ margin-bottom: 30px;
105
+ }
106
+
107
+ .info-card {
108
+ background: #f8f9fa;
109
+ padding: 25px;
110
+ border-radius: 15px;
111
+ border-left: 4px solid #007bff;
112
+ }
113
+
114
+ .info-card h3 {
115
+ color: #007bff;
116
+ margin-bottom: 15px;
117
+ }
118
+
119
+ .info-card .value {
120
+ font-family: Monaco, monospace;
121
+ background: white;
122
+ padding: 10px;
123
+ border-radius: 8px;
124
+ font-weight: bold;
125
+ }
126
+
127
+ .models-grid {
128
+ display: grid;
129
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
130
+ gap: 20px;
131
+ margin-bottom: 30px;
132
+ }
133
+
134
+ .model-card {
135
+ background: white;
136
+ border: 2px solid #e9ecef;
137
+ border-radius: 15px;
138
+ padding: 20px;
139
+ cursor: pointer;
140
+ transition: all 0.3s ease;
141
+ text-align: center;
142
+ }
143
+
144
+ .model-card:hover {
145
+ border-color: #007bff;
146
+ transform: translateY(-5px);
147
+ box-shadow: 0 10px 25px rgba(0, 123, 255, 0.2);
148
+ }
149
+
150
+ .model-card.selected {
151
+ border-color: #007bff;
152
+ background: #f8f9ff;
153
+ }
154
+
155
+ .model-card .model-name {
156
+ font-size: 1.2rem;
157
+ font-weight: bold;
158
+ color: #007bff;
159
+ margin-bottom: 8px;
160
+ }
161
+
162
+ .model-card .provider {
163
+ font-size: 0.9rem;
164
+ color: #6c757d;
165
+ opacity: 0.8;
166
+ }
167
+
168
+ .code-block {
169
+ background: #f8f9fa;
170
+ border: 1px solid #e9ecef;
171
+ border-radius: 10px;
172
+ padding: 20px;
173
+ margin: 20px 0;
174
+ position: relative;
175
+ }
176
+
177
+ .copy-btn {
178
+ position: absolute;
179
+ top: 10px;
180
+ right: 10px;
181
+ background: #007bff;
182
+ color: white;
183
+ border: none;
184
+ padding: 8px 15px;
185
+ border-radius: 6px;
186
+ cursor: pointer;
187
+ font-size: 0.9rem;
188
+ transition: background 0.3s ease;
189
+ }
190
+
191
+ .copy-btn:hover {
192
+ background: #0056b3;
193
+ }
194
+
195
+ .code-block code {
196
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
197
+ font-size: 0.9rem;
198
+ line-height: 1.5;
199
+ display: block;
200
+ white-space: pre-wrap;
201
+ word-break: break-all;
202
+ }
203
+
204
+ .endpoint-card {
205
+ background: #f8f9fa;
206
+ border-radius: 10px;
207
+ padding: 20px;
208
+ margin: 15px 0;
209
+ border-left: 4px solid #007bff;
210
+ }
211
+
212
+ .method-badge {
213
+ display: inline-block;
214
+ padding: 4px 12px;
215
+ border-radius: 20px;
216
+ font-size: 0.8rem;
217
+ font-weight: bold;
218
+ margin-right: 10px;
219
+ }
220
+
221
+ .method-get {
222
+ background: #28a745;
223
+ color: white;
224
+ }
225
+
226
+ .method-post {
227
+ background: #007bff;
228
+ color: white;
229
+ }
230
+
231
+ .footer {
232
+ text-align: center;
233
+ padding: 30px;
234
+ color: #6c757d;
235
+ background: rgba(255, 255, 255, 0.9);
236
+ margin-top: 40px;
237
+ border-radius: 15px;
238
+ }
239
+
240
+ @media (max-width: 768px) {
241
+ .header h1 {
242
+ font-size: 2rem;
243
+ }
244
+
245
+ .models-grid {
246
+ grid-template-columns: 1fr;
247
+ }
248
+
249
+ .api-info {
250
+ grid-template-columns: 1fr;
251
+ }
252
+ }
253
+ </style>
254
+ </head>
255
+
256
+ <body>
257
+ <div class="container">
258
+ <div class="header">
259
+ <h1>🚀 Cursor2API</h1>
260
+ <p class="subtitle">强大的AI模型API代理服务 - OpenAI兼容接口</p>
261
+ </div>
262
+
263
+ <div class="main-content">
264
+ <!-- 导航标签 -->
265
+ <div class="nav-tabs">
266
+ <button class="nav-tab active" onclick="showTab('overview')">概览</button>
267
+ <button class="nav-tab" onclick="showTab('models')">模型列表</button>
268
+ <button class="nav-tab" onclick="showTab('api')">API文档</button>
269
+ </div>
270
+
271
+ <!-- 概览页面 -->
272
+ <div id="overview" class="tab-content active">
273
+ <div class="status-card">
274
+ <h2>✅ 服务运行正常</h2>
275
+ <p> Cursor2API已成功启动并运行中</p>
276
+ </div>
277
+
278
+ <div class="api-info">
279
+ <div class="info-card">
280
+ <h3>📍 服务地址</h3>
281
+ <div class="value">http://localhost:8002</div>
282
+ </div>
283
+ <div class="info-card">
284
+ <h3>🔑 API密钥</h3>
285
+ <div class="value">0000 (默认)</div>
286
+ </div>
287
+ <div class="info-card">
288
+ <h3>🎯 兼容性</h3>
289
+ <div class="value">OpenAI API 标准</div>
290
+ </div>
291
+ <div class="info-card">
292
+ <h3>🌊 流式支持</h3>
293
+ <div class="value">支持 SSE 流式响应</div>
294
+ </div>
295
+ </div>
296
+
297
+ <div style="margin-top: 30px;">
298
+ <h3>🚀 快速开始</h3>
299
+
300
+ <div class="code-block">
301
+ <button class="copy-btn" onclick="copyCode(this)">复制</button>
302
+ <code># 获取模型列表
303
+ curl -H "Authorization: Bearer 0000" http://localhost:8002/v1/models</code>
304
+ </div>
305
+
306
+ <h4 style="margin-top: 15px; color: #555;">非流式聊天 (Non-Streaming)</h4>
307
+ <div class="code-block">
308
+ <button class="copy-btn" onclick="copyCode(this)">复制</button>
309
+ <code>curl -X POST http://localhost:8002/v1/chat/completions \
310
+ -H "Content-Type: application/json" \
311
+ -H "Authorization: Bearer 0000" \
312
+ -d '{
313
+ "model": "claude-sonnet-4.6",
314
+ "messages": [{"role": "user", "content": "Hello!"}],
315
+ "stream": false
316
+ }'</code>
317
+ </div>
318
+
319
+ <h4 style="margin-top: 15px; color: #555;">流式聊天 (Streaming)</h4>
320
+ <div class="code-block">
321
+ <button class="copy-btn" onclick="copyCode(this)">复制</button>
322
+ <code>curl -X POST http://localhost:8002/v1/chat/completions \
323
+ -H "Content-Type: application/json" \
324
+ -H "Authorization: Bearer 0000" \
325
+ -d '{
326
+ "model": "claude-sonnet-4.6",
327
+ "messages": [{"role": "user", "content": "Hello!"}],
328
+ "stream": true
329
+ }'</code>
330
+ </div>
331
+ </div>
332
+ </div>
333
+
334
+ <!-- 模型页面 -->
335
+ <div id="models" class="tab-content">
336
+ <h2>🤖 支持的AI模型</h2>
337
+ <p>点击模型卡片可查看详细信息和使用示例</p>
338
+
339
+ <div class="models-grid" id="modelsGrid">
340
+ <div class="model-card" onclick="selectModel('claude-sonnet-4.6')">
341
+ <div class="model-name">claude-sonnet-4.6</div>
342
+ <div class="provider">Anthropic Claude</div>
343
+ </div>
344
+ </div>
345
+
346
+ <div id="selectedModelInfo" style="display: none; margin-top: 30px;">
347
+ <h3>使用选中的模型</h3>
348
+ <div class="code-block">
349
+ <button class="copy-btn" onclick="copyCode(this)">复制</button>
350
+ <code id="modelExample"></code>
351
+ </div>
352
+ </div>
353
+ </div>
354
+
355
+ <!-- API页面 -->
356
+ <div id="api" class="tab-content">
357
+ <h2>📡 API端点文档</h2>
358
+
359
+ <div class="endpoint-card">
360
+ <div class="method-badge method-get">GET</div>
361
+ <strong>/v1/models</strong>
362
+ <p>获取所有可用的AI模型列表</p>
363
+ <div class="code-block" style="margin-top: 15px;">
364
+ <button class="copy-btn" onclick="copyCode(this)">复制</button>
365
+ <code>curl -H "Authorization: Bearer 0000" http://localhost:8002/v1/models</code>
366
+ </div>
367
+ </div>
368
+
369
+ <div class="endpoint-card">
370
+ <div class="method-badge method-post">POST</div>
371
+ <strong>/v1/chat/completions</strong>
372
+ <p>创建聊天完成请求,支持流式和非流式响应</p>
373
+ <div class="code-block" style="margin-top: 15px;">
374
+ <button class="copy-btn" onclick="copyCode(this)">复制</button>
375
+ <code>curl -X POST http://localhost:8002/v1/chat/completions \
376
+ -H "Content-Type: application/json" \
377
+ -H "Authorization: Bearer 0000" \
378
+ -d '{
379
+ "model": "claude-sonnet-4.6",
380
+ "messages": [
381
+ {"role": "user", "content": "你好"}
382
+ ],
383
+ "stream": false
384
+ }'</code>
385
+ </div>
386
+ </div>
387
+
388
+ <div class="endpoint-card">
389
+ <div class="method-badge method-get">GET</div>
390
+ <strong>/health</strong>
391
+ <p>健康检查端点</p>
392
+ <div class="code-block" style="margin-top: 15px;">
393
+ <button class="copy-btn" onclick="copyCode(this)">复制</button>
394
+ <code>curl http://localhost:8002/health</code>
395
+ </div>
396
+ </div>
397
+ </div>
398
+ </div>
399
+
400
+ <div class="footer">
401
+ <p>©2025 Cursor2API | 强大的AI模型API代理服务</p>
402
+ </div>
403
+ </div>
404
+
405
+ <script>
406
+ // 切换标签页
407
+ function showTab(tabName) {
408
+ const tabContents = document.getElementsByClassName('tab-content');
409
+ for (let i = 0; i < tabContents.length; i++) {
410
+ tabContents[i].classList.remove('active');
411
+ }
412
+
413
+ const tabBtns = document.getElementsByClassName('nav-tab');
414
+ for (let i = 0; i < tabBtns.length; i++) {
415
+ tabBtns[i].classList.remove('active');
416
+ }
417
+
418
+ document.getElementById(tabName).classList.add('active');
419
+ event.target.classList.add('active');
420
+ }
421
+
422
+ // 选择模型
423
+ function selectModel(modelId) {
424
+ const prevSelected = document.querySelector('.model-card.selected');
425
+ if (prevSelected) {
426
+ prevSelected.classList.remove('selected');
427
+ }
428
+
429
+ const modelCards = document.querySelectorAll('.model-card');
430
+ modelCards.forEach(card => {
431
+ if (card.onclick.toString().includes(modelId)) {
432
+ card.classList.add('selected');
433
+ }
434
+ });
435
+
436
+ const exampleCode = `curl -X POST http://localhost:8002/v1/chat/completions \\
437
+ -H "Content-Type: application/json" \\
438
+ -H "Authorization: Bearer 0000" \\
439
+ -d '{
440
+ "model": "${modelId}",
441
+ "messages": [
442
+ {
443
+ "role": "user",
444
+ "content": "你好,请用中文回答问题"
445
+ }
446
+ ],
447
+ "stream": false
448
+ }'`;
449
+
450
+ document.getElementById('modelExample').textContent = exampleCode;
451
+ document.getElementById('selectedModelInfo').style.display = 'block';
452
+ }
453
+
454
+ // 复制代码功能
455
+ function copyCode(btn) {
456
+ const codeBlock = btn.nextElementSibling;
457
+ const code = codeBlock.textContent;
458
+
459
+ navigator.clipboard.writeText(code).then(() => {
460
+ const originalText = btn.textContent;
461
+ btn.textContent = '已复制!';
462
+ btn.style.background = '#28a745';
463
+ setTimeout(() => {
464
+ btn.textContent = originalText;
465
+ btn.style.background = '#007bff';
466
+ }, 2000);
467
+ }).catch(() => {
468
+ alert('复制失败,请手动复制代码');
469
+ });
470
+ }
471
+ </script>
472
+ </body>
473
+
474
+ </html>
utils/headers.go ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package utils
22
+
23
+ import (
24
+ "fmt"
25
+ "math/rand"
26
+ "runtime"
27
+ "time"
28
+ )
29
+
30
+ // BrowserProfile 浏览器配置文件
31
+ type BrowserProfile struct {
32
+ Platform string
33
+ PlatformVersion string
34
+ Architecture string
35
+ Bitness string
36
+ ChromeVersion int
37
+ UserAgent string
38
+ Mobile bool
39
+ }
40
+
41
+ var (
42
+ // 常见的浏览器版本 (Chrome)
43
+ chromeVersions = []int{120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130}
44
+
45
+ // Windows 平台配置
46
+ windowsProfiles = []BrowserProfile{
47
+ {Platform: "Windows", PlatformVersion: "10.0.0", Architecture: "x86", Bitness: "64"},
48
+ {Platform: "Windows", PlatformVersion: "11.0.0", Architecture: "x86", Bitness: "64"},
49
+ {Platform: "Windows", PlatformVersion: "15.0.0", Architecture: "x86", Bitness: "64"},
50
+ }
51
+
52
+ // macOS 平台配置
53
+ macosProfiles = []BrowserProfile{
54
+ {Platform: "macOS", PlatformVersion: "13.0.0", Architecture: "arm", Bitness: "64"},
55
+ {Platform: "macOS", PlatformVersion: "14.0.0", Architecture: "arm", Bitness: "64"},
56
+ {Platform: "macOS", PlatformVersion: "15.0.0", Architecture: "arm", Bitness: "64"},
57
+ {Platform: "macOS", PlatformVersion: "13.0.0", Architecture: "x86", Bitness: "64"},
58
+ {Platform: "macOS", PlatformVersion: "14.0.0", Architecture: "x86", Bitness: "64"},
59
+ }
60
+
61
+ // Linux 平台配置
62
+ linuxProfiles = []BrowserProfile{
63
+ {Platform: "Linux", PlatformVersion: "", Architecture: "x86", Bitness: "64"},
64
+ }
65
+ )
66
+
67
+ // HeaderGenerator 动态 header 生成器
68
+ type HeaderGenerator struct {
69
+ profile BrowserProfile
70
+ chromeVersion int
71
+ rng *rand.Rand
72
+ }
73
+
74
+ // NewHeaderGenerator 创建新的 header 生成器
75
+ func NewHeaderGenerator() *HeaderGenerator {
76
+ // 使用当前时间作为随机种子
77
+ rng := rand.New(rand.NewSource(time.Now().UnixNano()))
78
+
79
+ // 根据当前操作系统选择合适的配置文件
80
+ var profiles []BrowserProfile
81
+ switch runtime.GOOS {
82
+ case "darwin":
83
+ profiles = macosProfiles
84
+ case "linux":
85
+ profiles = linuxProfiles
86
+ default:
87
+ profiles = windowsProfiles
88
+ }
89
+
90
+ // 随机选择一个配置文件
91
+ profile := profiles[rng.Intn(len(profiles))]
92
+
93
+ // 随机选择 Chrome 版本
94
+ chromeVersion := chromeVersions[rng.Intn(len(chromeVersions))]
95
+ profile.ChromeVersion = chromeVersion
96
+
97
+ // 生成 User-Agent
98
+ profile.UserAgent = generateUserAgent(profile)
99
+
100
+ return &HeaderGenerator{
101
+ profile: profile,
102
+ chromeVersion: chromeVersion,
103
+ rng: rng,
104
+ }
105
+ }
106
+
107
+ // generateUserAgent 生成 User-Agent 字符串
108
+ func generateUserAgent(profile BrowserProfile) string {
109
+ switch profile.Platform {
110
+ case "Windows":
111
+ return fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.0.0 Safari/537.36", profile.ChromeVersion)
112
+ case "macOS":
113
+ if profile.Architecture == "arm" {
114
+ return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.0.0 Safari/537.36", profile.ChromeVersion)
115
+ }
116
+ return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.0.0 Safari/537.36", profile.ChromeVersion)
117
+ case "Linux":
118
+ return fmt.Sprintf("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.0.0 Safari/537.36", profile.ChromeVersion)
119
+ default:
120
+ return fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%d.0.0.0 Safari/537.36", profile.ChromeVersion)
121
+ }
122
+ }
123
+
124
+ // GetChatHeaders 获取聊天请求的 headers
125
+ func (g *HeaderGenerator) GetChatHeaders(xIsHuman string) map[string]string {
126
+ // 随机选择语言
127
+ languages := []string{
128
+ "en-US,en;q=0.9",
129
+ "zh-CN,zh;q=0.9,en;q=0.8",
130
+ "en-GB,en;q=0.9",
131
+ }
132
+ lang := languages[g.rng.Intn(len(languages))]
133
+
134
+ // 随机选择 referer
135
+ referers := []string{
136
+ "https://cursor.com/en-US/learn/how-ai-models-work",
137
+ "https://cursor.com/cn/learn/how-ai-models-work",
138
+ "https://cursor.com/",
139
+ }
140
+ referer := referers[g.rng.Intn(len(referers))]
141
+
142
+ headers := map[string]string{
143
+ "sec-ch-ua-platform": fmt.Sprintf(`"%s"`, g.profile.Platform),
144
+ "x-path": "/api/chat",
145
+ "Referer": referer,
146
+ "sec-ch-ua": g.getSecChUa(),
147
+ "x-method": "POST",
148
+ "sec-ch-ua-mobile": "?0",
149
+ "x-is-human": xIsHuman,
150
+ "User-Agent": g.profile.UserAgent,
151
+ "content-type": "application/json",
152
+ "accept-language": lang,
153
+ }
154
+
155
+ // 添加可选的 headers
156
+ if g.profile.Architecture != "" {
157
+ headers["sec-ch-ua-arch"] = fmt.Sprintf(`"%s"`, g.profile.Architecture)
158
+ }
159
+ if g.profile.Bitness != "" {
160
+ headers["sec-ch-ua-bitness"] = fmt.Sprintf(`"%s"`, g.profile.Bitness)
161
+ }
162
+ if g.profile.PlatformVersion != "" {
163
+ headers["sec-ch-ua-platform-version"] = fmt.Sprintf(`"%s"`, g.profile.PlatformVersion)
164
+ }
165
+
166
+ return headers
167
+ }
168
+
169
+ // GetScriptHeaders 获取脚本请求的 headers
170
+ func (g *HeaderGenerator) GetScriptHeaders() map[string]string {
171
+ // 随机选择语言
172
+ languages := []string{
173
+ "en-US,en;q=0.9",
174
+ "zh-CN,zh;q=0.9,en;q=0.8",
175
+ "en-GB,en;q=0.9",
176
+ }
177
+ lang := languages[g.rng.Intn(len(languages))]
178
+
179
+ // 随机选择 referer
180
+ referers := []string{
181
+ "https://cursor.com/cn/learn/how-ai-models-work",
182
+ "https://cursor.com/en-US/learn/how-ai-models-work",
183
+ "https://cursor.com/",
184
+ }
185
+ referer := referers[g.rng.Intn(len(referers))]
186
+
187
+ headers := map[string]string{
188
+ "User-Agent": g.profile.UserAgent,
189
+ "sec-ch-ua-arch": fmt.Sprintf(`"%s"`, g.profile.Architecture),
190
+ "sec-ch-ua-platform": fmt.Sprintf(`"%s"`, g.profile.Platform),
191
+ "sec-ch-ua": g.getSecChUa(),
192
+ "sec-ch-ua-bitness": fmt.Sprintf(`"%s"`, g.profile.Bitness),
193
+ "sec-ch-ua-mobile": "?0",
194
+ "sec-fetch-site": "same-origin",
195
+ "sec-fetch-mode": "no-cors",
196
+ "sec-fetch-dest": "script",
197
+ "referer": referer,
198
+ "accept-language": lang,
199
+ }
200
+
201
+ if g.profile.PlatformVersion != "" {
202
+ headers["sec-ch-ua-platform-version"] = fmt.Sprintf(`"%s"`, g.profile.PlatformVersion)
203
+ }
204
+
205
+ return headers
206
+ }
207
+
208
+ // getSecChUa 生成 sec-ch-ua header
209
+ func (g *HeaderGenerator) getSecChUa() string {
210
+ // 生成随机的品牌版本
211
+ notABrand := 24 + g.rng.Intn(10) // 24-33
212
+
213
+ return fmt.Sprintf(`"Google Chrome";v="%d", "Chromium";v="%d", "Not(A:Brand";v="%d"`,
214
+ g.chromeVersion, g.chromeVersion, notABrand)
215
+ }
216
+
217
+ // GetUserAgent 获取 User-Agent
218
+ func (g *HeaderGenerator) GetUserAgent() string {
219
+ return g.profile.UserAgent
220
+ }
221
+
222
+ // GetProfile 获取浏览器配置文件
223
+ func (g *HeaderGenerator) GetProfile() BrowserProfile {
224
+ return g.profile
225
+ }
226
+
227
+ // Refresh 刷新配置文件(生成新的随机配置)
228
+ func (g *HeaderGenerator) Refresh() {
229
+ // 根据当前操作系统选择合适的配置文件
230
+ var profiles []BrowserProfile
231
+ switch runtime.GOOS {
232
+ case "darwin":
233
+ profiles = macosProfiles
234
+ case "linux":
235
+ profiles = linuxProfiles
236
+ default:
237
+ profiles = windowsProfiles
238
+ }
239
+
240
+ // 随机选择一个配置文件
241
+ profile := profiles[g.rng.Intn(len(profiles))]
242
+
243
+ // 随机选择 Chrome 版本
244
+ chromeVersion := chromeVersions[g.rng.Intn(len(chromeVersions))]
245
+ profile.ChromeVersion = chromeVersion
246
+
247
+ // 生成 User-Agent
248
+ profile.UserAgent = generateUserAgent(profile)
249
+
250
+ g.profile = profile
251
+ g.chromeVersion = chromeVersion
252
+ }
253
+
254
+ // GetRandomReferer 获取随机 referer
255
+ func GetRandomReferer() string {
256
+ referers := []string{
257
+ "https://cursor.com/en-US/learn/how-ai-models-work",
258
+ "https://cursor.com/cn/learn/how-ai-models-work",
259
+ "https://cursor.com/",
260
+ "https://cursor.com/features",
261
+ }
262
+ rng := rand.New(rand.NewSource(time.Now().UnixNano()))
263
+ return referers[rng.Intn(len(referers))]
264
+ }
265
+
266
+ // GetRandomLanguage 获取随机语言设置
267
+ func GetRandomLanguage() string {
268
+ languages := []string{
269
+ "en-US,en;q=0.9",
270
+ "zh-CN,zh;q=0.9,en;q=0.8",
271
+ "en-GB,en;q=0.9",
272
+ "ja-JP,ja;q=0.9,en;q=0.8",
273
+ }
274
+ rng := rand.New(rand.NewSource(time.Now().UnixNano()))
275
+ return languages[rng.Intn(len(languages))]
276
+ }
utils/utils.go ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Copyright (c) 2025-2026 libaxuan
2
+ //
3
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ // of this software and associated documentation files (the "Software"), to deal
5
+ // in the Software without restriction, including without limitation the rights
6
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ // copies of the Software, and to permit persons to whom the Software is
8
+ // furnished to do so, subject to the following conditions:
9
+ //
10
+ // The above copyright notice and this permission notice shall be included in all
11
+ // copies or substantial portions of the Software.
12
+ //
13
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ // SOFTWARE.
20
+
21
+ package utils
22
+
23
+ import (
24
+ "bufio"
25
+ "context"
26
+ "crypto/rand"
27
+ "cursor2api-go/middleware"
28
+ "cursor2api-go/models"
29
+ "encoding/hex"
30
+ "encoding/json"
31
+ "fmt"
32
+ "io"
33
+ "net/http"
34
+ "os/exec"
35
+ "strings"
36
+ "time"
37
+
38
+ "github.com/gin-gonic/gin"
39
+ "github.com/sirupsen/logrus"
40
+ )
41
+
42
+ // GenerateRandomString 生成指定长度的随机字符串
43
+ func GenerateRandomString(length int) string {
44
+ if length <= 0 {
45
+ return ""
46
+ }
47
+
48
+ byteLen := (length + 1) / 2
49
+ bytes := make([]byte, byteLen)
50
+ if _, err := rand.Read(bytes); err != nil {
51
+ fallback := fmt.Sprintf("%d", time.Now().UnixNano())
52
+ if len(fallback) >= length {
53
+ return fallback[:length]
54
+ }
55
+ return fallback
56
+ }
57
+
58
+ encoded := hex.EncodeToString(bytes)
59
+ if len(encoded) < length {
60
+ encoded += GenerateRandomString(length - len(encoded))
61
+ }
62
+
63
+ return encoded[:length]
64
+ }
65
+
66
+ // GenerateChatCompletionID 生成聊天完成ID
67
+ func GenerateChatCompletionID() string {
68
+ return "chatcmpl-" + GenerateRandomString(29)
69
+ }
70
+
71
+ // ParseSSELine 解析SSE数据行
72
+ func ParseSSELine(line string) string {
73
+ line = strings.TrimSpace(line)
74
+ if strings.HasPrefix(line, "data: ") {
75
+ return strings.TrimSpace(line[6:]) // 去掉 'data: ' 前缀并去除前导空格
76
+ }
77
+ return ""
78
+ }
79
+
80
+ // WriteSSEEvent 写入SSE事件
81
+ func WriteSSEEvent(w http.ResponseWriter, event, data string) error {
82
+ if event != "" {
83
+ if _, err := fmt.Fprintf(w, "event: %s\n", event); err != nil {
84
+ return err
85
+ }
86
+ }
87
+ if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil {
88
+ return err
89
+ }
90
+
91
+ // 刷新缓冲区
92
+ if flusher, ok := w.(http.Flusher); ok {
93
+ flusher.Flush()
94
+ }
95
+
96
+ return nil
97
+ }
98
+
99
+ // StreamChatCompletion 处理流式聊天完成
100
+ // StreamChatCompletion 处理流式聊天完成
101
+ func StreamChatCompletion(c *gin.Context, chatGenerator <-chan interface{}, modelName string) {
102
+ // 设置SSE头
103
+ c.Header("Content-Type", "text/event-stream")
104
+ c.Header("Cache-Control", "no-cache")
105
+ c.Header("Connection", "keep-alive")
106
+ c.Header("Access-Control-Allow-Origin", "*")
107
+
108
+ // 生成响应ID
109
+ responseID := GenerateChatCompletionID()
110
+
111
+ // 处理流式数据
112
+ ctx := c.Request.Context()
113
+ for {
114
+ select {
115
+ case <-ctx.Done():
116
+ logrus.Debug("Client disconnected during streaming")
117
+ return
118
+
119
+ case data, ok := <-chatGenerator:
120
+ if !ok {
121
+ // 通道关闭,发送完成事件
122
+ finishEvent := models.NewChatCompletionStreamResponse(responseID, modelName, "", stringPtr("stop"))
123
+ if jsonData, err := json.Marshal(finishEvent); err == nil {
124
+ WriteSSEEvent(c.Writer, "", string(jsonData))
125
+ }
126
+ WriteSSEEvent(c.Writer, "", "[DONE]")
127
+ return
128
+ }
129
+
130
+ switch v := data.(type) {
131
+ case string:
132
+ // 文本内容
133
+ if v != "" {
134
+ streamResp := models.NewChatCompletionStreamResponse(responseID, modelName, v, nil)
135
+ if jsonData, err := json.Marshal(streamResp); err == nil {
136
+ WriteSSEEvent(c.Writer, "", string(jsonData))
137
+ }
138
+ }
139
+
140
+ case models.Usage:
141
+ // 使用统计 - 通常在最后发送
142
+ continue
143
+
144
+ case error:
145
+ logrus.WithError(v).Error("Stream generator error")
146
+ WriteSSEEvent(c.Writer, "", "[DONE]")
147
+ return
148
+
149
+ default:
150
+ logrus.Warnf("Unknown data type in stream: %T", v)
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ // NonStreamChatCompletion 处理非流式聊天完成
157
+ func NonStreamChatCompletion(c *gin.Context, chatGenerator <-chan interface{}, modelName string) {
158
+ var fullContent strings.Builder
159
+ var usage models.Usage
160
+
161
+ // 收集所有数据
162
+ ctx := c.Request.Context()
163
+ for {
164
+ select {
165
+ case <-ctx.Done():
166
+ c.JSON(http.StatusRequestTimeout, models.NewErrorResponse(
167
+ "Request timeout",
168
+ "timeout_error",
169
+ "request_timeout",
170
+ ))
171
+ return
172
+
173
+ case data, ok := <-chatGenerator:
174
+ if !ok {
175
+ // 数据收集完成,返回响应
176
+ responseID := GenerateChatCompletionID()
177
+ response := models.NewChatCompletionResponse(
178
+ responseID,
179
+ modelName,
180
+ fullContent.String(),
181
+ usage,
182
+ )
183
+ c.JSON(http.StatusOK, response)
184
+ return
185
+ }
186
+
187
+ switch v := data.(type) {
188
+ case string:
189
+ fullContent.WriteString(v)
190
+ case models.Usage:
191
+ usage = v
192
+ case error:
193
+ middleware.HandleError(c, v)
194
+ return
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ // ErrorWrapper 错误包装器
201
+ func ErrorWrapper(handler func(*gin.Context) error) gin.HandlerFunc {
202
+ return func(c *gin.Context) {
203
+ if err := handler(c); err != nil {
204
+ logrus.WithError(err).Error("Handler error")
205
+
206
+ if !c.Writer.Written() {
207
+ c.JSON(http.StatusInternalServerError, models.NewErrorResponse(
208
+ "Internal server error",
209
+ "internal_error",
210
+ "",
211
+ ))
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ // SafeStreamWrapper 安全流式包装器
218
+ func SafeStreamWrapper(handler func(*gin.Context, <-chan interface{}, string), c *gin.Context, chatGenerator <-chan interface{}, modelName string) {
219
+ defer func() {
220
+ if r := recover(); r != nil {
221
+ logrus.WithField("panic", r).Error("Panic in stream handler")
222
+ if !c.Writer.Written() {
223
+ c.JSON(http.StatusInternalServerError, models.NewErrorResponse(
224
+ "Internal server error",
225
+ "panic_error",
226
+ "",
227
+ ))
228
+ }
229
+ }
230
+ }()
231
+
232
+ firstItem, ok := <-chatGenerator
233
+ if !ok {
234
+ middleware.HandleError(c, middleware.NewCursorWebError(http.StatusInternalServerError, "empty stream"))
235
+ return
236
+ }
237
+
238
+ if err, isErr := firstItem.(error); isErr {
239
+ middleware.HandleError(c, err)
240
+ return
241
+ }
242
+
243
+ buffered := make(chan interface{}, 1)
244
+ buffered <- firstItem
245
+ ctx := c.Request.Context()
246
+
247
+ go func() {
248
+ defer close(buffered)
249
+ for {
250
+ select {
251
+ case <-ctx.Done():
252
+ return
253
+ case item, ok := <-chatGenerator:
254
+ if !ok {
255
+ return
256
+ }
257
+ select {
258
+ case buffered <- item:
259
+ case <-ctx.Done():
260
+ return
261
+ }
262
+ }
263
+ }
264
+ }()
265
+
266
+ handler(c, buffered, modelName)
267
+ }
268
+
269
+ // CreateHTTPClient 创建HTTP客户端
270
+ func CreateHTTPClient(timeout time.Duration) *http.Client {
271
+ return &http.Client{
272
+ Timeout: timeout,
273
+ Transport: &http.Transport{
274
+ MaxIdleConns: 100,
275
+ MaxIdleConnsPerHost: 10,
276
+ IdleConnTimeout: 90 * time.Second,
277
+ },
278
+ }
279
+ }
280
+
281
+ // ReadSSEStream 读取SSE流
282
+ func ReadSSEStream(ctx context.Context, resp *http.Response, output chan<- interface{}) error {
283
+ scanner := bufio.NewScanner(resp.Body)
284
+ scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
285
+ defer resp.Body.Close()
286
+
287
+ for scanner.Scan() {
288
+ select {
289
+ case <-ctx.Done():
290
+ return ctx.Err()
291
+ default:
292
+ }
293
+
294
+ line := scanner.Text()
295
+ data := ParseSSELine(line)
296
+ if data == "" {
297
+ continue
298
+ }
299
+
300
+ if data == "[DONE]" {
301
+ return nil
302
+ }
303
+
304
+ // 尝试解析JSON数据
305
+ var eventData models.CursorEventData
306
+ if err := json.Unmarshal([]byte(data), &eventData); err != nil {
307
+ logrus.WithError(err).Debugf("Failed to parse SSE data: %s", data)
308
+ continue
309
+ }
310
+
311
+ // 处理不同类型的事件
312
+ switch eventData.Type {
313
+ case "error":
314
+ if eventData.ErrorText != "" {
315
+ return fmt.Errorf("cursor API error: %s", eventData.ErrorText)
316
+ }
317
+
318
+ case "finish":
319
+ if eventData.MessageMetadata != nil && eventData.MessageMetadata.Usage != nil {
320
+ usage := models.Usage{
321
+ PromptTokens: eventData.MessageMetadata.Usage.InputTokens,
322
+ CompletionTokens: eventData.MessageMetadata.Usage.OutputTokens,
323
+ TotalTokens: eventData.MessageMetadata.Usage.TotalTokens,
324
+ }
325
+ output <- usage
326
+ }
327
+ return nil
328
+
329
+ default:
330
+ if eventData.Delta != "" {
331
+ output <- eventData.Delta
332
+ }
333
+ }
334
+ }
335
+
336
+ return scanner.Err()
337
+ }
338
+
339
+ // ValidateModel 验证模型名称
340
+ func ValidateModel(model string, validModels []string) bool {
341
+ for _, validModel := range validModels {
342
+ if validModel == model {
343
+ return true
344
+ }
345
+ }
346
+ return false
347
+ }
348
+
349
+ // SanitizeContent 清理内容
350
+ func SanitizeContent(content string) string {
351
+ // 移除可能的恶意内容
352
+ content = strings.ReplaceAll(content, "\x00", "")
353
+ return content
354
+ }
355
+
356
+ // stringPtr 返回字符串指针
357
+ func stringPtr(s string) *string {
358
+ return &s
359
+ }
360
+
361
+ // CopyHeaders 复制HTTP头
362
+ func CopyHeaders(dst, src http.Header, skipHeaders []string) {
363
+ skipMap := make(map[string]bool)
364
+ for _, header := range skipHeaders {
365
+ skipMap[strings.ToLower(header)] = true
366
+ }
367
+
368
+ for key, values := range src {
369
+ if skipMap[strings.ToLower(key)] {
370
+ continue
371
+ }
372
+ for _, value := range values {
373
+ dst.Add(key, value)
374
+ }
375
+ }
376
+ }
377
+
378
+ // IsJSONContentType 检查是否为JSON内容类型
379
+ func IsJSONContentType(contentType string) bool {
380
+ return strings.Contains(strings.ToLower(contentType), "application/json")
381
+ }
382
+
383
+ // ReadRequestBody 读取请求体
384
+ func ReadRequestBody(r *http.Request) ([]byte, error) {
385
+ if r.Body == nil {
386
+ return nil, nil
387
+ }
388
+
389
+ body, err := io.ReadAll(r.Body)
390
+ if err != nil {
391
+ return nil, fmt.Errorf("failed to read request body: %w", err)
392
+ }
393
+
394
+ return body, nil
395
+ }
396
+
397
+ // RunJS 执行JavaScript代码并返回标准输出内容
398
+ func RunJS(jsCode string) (string, error) {
399
+ // 添加crypto模块导入并设置为全局变量
400
+ // 注意:使用stdin时,我们需要确保代码是自包含的
401
+ finalJS := `const crypto = require('crypto').webcrypto;
402
+ global.crypto = crypto;
403
+ globalThis.crypto = crypto;
404
+ // 在Node.js环境中创建window对象
405
+ if (typeof window === 'undefined') { global.window = global; }
406
+ window.crypto = crypto;
407
+ this.crypto = crypto;
408
+ ` + jsCode
409
+
410
+ // 执行Node.js命令,使用stdin输入代码
411
+ cmd := exec.Command("node")
412
+
413
+ // 设置输入
414
+ cmd.Stdin = strings.NewReader(finalJS)
415
+
416
+ output, err := cmd.Output()
417
+ if err != nil {
418
+ if exitErr, ok := err.(*exec.ExitError); ok {
419
+ return "", fmt.Errorf("node.js execution failed (exit code: %d)\nSTDOUT:\n%s\nSTDERR:\n%s",
420
+ exitErr.ExitCode(), string(output), string(exitErr.Stderr))
421
+ }
422
+ return "", fmt.Errorf("failed to execute node.js: %w", err)
423
+ }
424
+
425
+ return strings.TrimSpace(string(output)), nil
426
+ }