Upload 27 files
Browse files- Dockerfile +54 -0
- LICENSE +21 -0
- TROUBLESHOOTING.md +114 -0
- config/config.go +188 -0
- config/config_test.go +200 -0
- docker-compose.yml +42 -0
- docs/DYNAMIC_HEADERS.md +180 -0
- docs/STARTUP_OPTIMIZATION.md +168 -0
- go.mod +51 -0
- go.sum +124 -0
- handlers/handler.go +269 -0
- jscode/env.js +0 -0
- jscode/main.js +128 -0
- main.go +164 -0
- middleware/auth.go +79 -0
- middleware/cors.go +45 -0
- middleware/error.go +188 -0
- models/model_config.go +101 -0
- models/models.go +336 -0
- models/models_test.go +168 -0
- services/cursor.go +362 -0
- start-go-utf8.bat +128 -0
- start-go.bat +128 -0
- start.sh +133 -0
- static/docs.html +474 -0
- utils/headers.go +276 -0
- utils/utils.go +426 -0
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 |
+
}
|