yunzerwebsiteallinone/go/pkg/tokenprobe/cursor_hi.go
2026-06-05 13:18:57 +08:00

728 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package tokenprobe
import (
"bytes"
"compress/gzip"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"regexp"
"runtime"
"strings"
"time"
"unicode/utf8"
"github.com/google/uuid"
"golang.org/x/net/http2"
)
const (
cursorBackendURL = "https://api2.cursor.sh"
cursorAgentPath = "/aiserver.v1.ChatService/StreamUnifiedChatWithTools"
cursorClientVersion = "3.6.31"
cursorHiMaxRead = 512 * 1024
// probeHiText 发往官方 Agent 的探测内容(与前端展示 probeMessage 一致)
probeHiText = "hi"
)
var cursorProbeHTTPClient = newCursorHTTP2Client()
func newCursorHTTP2Client() *http.Client {
tr := &http.Transport{
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
}
// 与 Cursor 官方一致走 HTTP/2
if err := http2.ConfigureTransport(tr); err != nil {
return &http.Client{Timeout: 40 * time.Second}
}
return &http.Client{Transport: tr, Timeout: 40 * time.Second}
}
func cursorClientOS() string {
switch runtime.GOOS {
case "windows":
return "win32"
case "darwin":
return "darwin"
default:
return "linux"
}
}
func cursorClientArch() string {
switch runtime.GOARCH {
case "amd64":
return "x64"
case "arm64":
return "arm64"
default:
return runtime.GOARCH
}
}
func cursorEnvVersion() string {
if v := strings.TrimSpace(os.Getenv("CURSOR_CLIENT_VERSION")); v != "" {
return v
}
return cursorClientVersion
}
// --- protobuf wire (与 cursor_api_demo 对齐) ---
func pbVarint(v uint64) []byte {
var out []byte
for v >= 0x80 {
out = append(out, byte(v&0x7f|0x80))
v >>= 7
}
out = append(out, byte(v&0x7f))
return out
}
func pbField(fieldNum int, wireType int, value interface{}) []byte {
tag := uint64(fieldNum<<3 | wireType)
out := pbVarint(tag)
switch wireType {
case 0:
var n uint64
switch x := value.(type) {
case int:
n = uint64(x)
case int32:
n = uint64(x)
case uint32:
n = uint64(x)
case uint64:
n = x
default:
n = uint64(0)
}
out = append(out, pbVarint(n)...)
case 2:
var b []byte
switch x := value.(type) {
case string:
b = []byte(x)
case []byte:
b = x
default:
b = []byte(fmt.Sprint(x))
}
out = append(out, pbVarint(uint64(len(b)))...)
out = append(out, b...)
}
return out
}
func encodeCursorMessage(content string, role int, messageID string, chatModeEnum *int) []byte {
msg := pbField(1, 2, content)
msg = append(msg, pbField(2, 0, role)...)
msg = append(msg, pbField(13, 2, messageID)...)
if chatModeEnum != nil {
msg = append(msg, pbField(47, 0, *chatModeEnum)...)
}
return msg
}
func encodeCursorModel(modelName string) []byte {
msg := pbField(1, 2, modelName)
msg = append(msg, pbField(4, 2, []byte{})...)
return msg
}
func encodeCursorSetting() []byte {
inner := pbField(1, 2, []byte{})
inner = append(inner, pbField(2, 2, []byte{})...)
msg := pbField(1, 2, `cursor\aisettings`)
msg = append(msg, pbField(3, 2, []byte{})...)
msg = append(msg, pbField(6, 2, inner)...)
msg = append(msg, pbField(8, 0, 1)...)
msg = append(msg, pbField(9, 0, 1)...)
return msg
}
func encodeCursorMetadata() []byte {
msg := pbField(1, 2, cursorClientOS())
msg = append(msg, pbField(2, 2, cursorClientArch())...)
msg = append(msg, pbField(3, 2, "unknown")...)
msg = append(msg, pbField(4, 2, "go-platform/tokenprobe")...)
msg = append(msg, pbField(5, 2, time.Now().Format(time.RFC3339))...)
return msg
}
func encodeCursorMessageID(messageID string, role int) []byte {
msg := pbField(1, 2, messageID)
msg = append(msg, pbField(3, 0, role)...)
return msg
}
// defaultAgentTools 与 cursor_agent_client.DEFAULT_TOOLS 一致
var defaultAgentTools = []int{5, 6, 3, 15, 7, 8, 42}
func encodeCursorAgentRequest(userContent, modelName string) []byte {
msgID := uuid.NewString()
cm := 2 // Agent
userMsg := encodeCursorMessage(userContent, 1, msgID, &cm)
var msg []byte
msg = append(msg, pbField(1, 2, userMsg)...)
msg = append(msg, pbField(2, 0, 1)...)
msg = append(msg, pbField(3, 2, []byte{})...)
msg = append(msg, pbField(4, 0, 1)...)
msg = append(msg, pbField(5, 2, encodeCursorModel(modelName))...)
msg = append(msg, pbField(8, 2, "")...)
msg = append(msg, pbField(13, 0, 1)...)
msg = append(msg, pbField(15, 2, encodeCursorSetting())...)
msg = append(msg, pbField(19, 0, 1)...)
msg = append(msg, pbField(23, 2, uuid.NewString())...)
msg = append(msg, pbField(26, 2, encodeCursorMetadata())...)
msg = append(msg, pbField(27, 0, 1)...)
for _, t := range defaultAgentTools {
msg = append(msg, pbField(29, 0, t)...)
}
msg = append(msg, pbField(30, 2, encodeCursorMessageID(msgID, 1))...)
msg = append(msg, pbField(35, 0, 0)...)
msg = append(msg, pbField(38, 0, 0)...)
msg = append(msg, pbField(46, 0, 2)...)
msg = append(msg, pbField(47, 2, "")...)
msg = append(msg, pbField(48, 0, 0)...)
msg = append(msg, pbField(49, 0, 0)...)
msg = append(msg, pbField(51, 0, 0)...)
msg = append(msg, pbField(53, 0, 1)...)
msg = append(msg, pbField(54, 2, "agent")...)
return msg
}
func encodeStreamUnifiedChatWithToolsRequest(inner []byte) []byte {
return pbField(1, 2, inner)
}
func generateCursorAgentFramedBody(userText, model string) []byte {
inner := encodeCursorAgentRequest(userText, model)
buf := encodeStreamUnifiedChatWithToolsRequest(inner)
magic := byte(0x00)
hexLen := fmt.Sprintf("%08x", len(buf))
lenBytes, err := hex.DecodeString(hexLen)
if err != nil || len(lenBytes) != 4 {
lenB := []byte{byte(len(buf) >> 24), byte(len(buf) >> 16), byte(len(buf) >> 8), byte(len(buf))}
return append([]byte{magic}, append(lenB, buf...)...)
}
return append([]byte{magic}, append(lenBytes, buf...)...)
}
func hashed64Hex(input, salt string) string {
h := sha256.Sum256([]byte(input + salt))
return hex.EncodeToString(h[:])
}
func generateCursorChecksum(authToken string) string {
machineID := hashed64Hex(authToken, "machineId")
ts := int(time.Now().UnixMilli() / 1_000_000)
barr := []byte{
byte(ts >> 40), byte(ts >> 32), byte(ts >> 24), byte(ts >> 16), byte(ts >> 8), byte(ts),
}
t := byte(165)
for i := range barr {
barr[i] = ((barr[i] ^ t) + byte(i%256)) & 255
t = barr[i]
}
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
var enc strings.Builder
for i := 0; i < len(barr); i += 3 {
a := barr[i]
var b, c byte
if i+1 < len(barr) {
b = barr[i+1]
}
if i+2 < len(barr) {
c = barr[i+2]
}
enc.WriteByte(alphabet[a>>2])
enc.WriteByte(alphabet[((a&3)<<4)|(b>>4)])
if i+1 < len(barr) {
enc.WriteByte(alphabet[((b&15)<<2)|(c>>6)])
}
if i+2 < len(barr) {
enc.WriteByte(alphabet[c&63])
}
}
return enc.String() + machineID
}
func asciiLowerInPlace(b []byte) {
for i := range b {
c := b[i]
if c >= 'A' && c <= 'Z' {
b[i] = c + ('a' - 'A')
}
}
}
// 社区脚本中的「额度用尽」ASCII 前缀(与明文一致,便于在二进制流中 bytes.Contains无需整句
// 对应明文前缀Get Cursor Pro for more Agent usage
var cursorQuotaExhaustedSigCommunity = []byte{
0x47, 0x65, 0x74, 0x20, 0x43, 0x75, 0x72, 0x73,
0x6f, 0x72, 0x20, 0x50, 0x72, 0x6f, 0x20, 0x66,
0x6f, 0x72, 0x20, 0x6d, 0x6f, 0x72, 0x65, 0x20,
0x41, 0x67, 0x65, 0x6e, 0x74, 0x20, 0x75, 0x73,
0x61, 0x67, 0x65,
}
// cursorQuotaTipSig 与常见示例一致raw 全字节里 bytes.Contains(raw, tipSig) → 额度用尽
var cursorQuotaTipSig = []byte("Get Cursor Pro for more Agent usage, unlimited Tab, and more.")
const cursorLimitTipPrefix = "Get Cursor Pro for more Agent usage, unlimited Tab"
// classifyCursorRawStream 在官方流式二进制/文本中匹配用量与升级提示
// 返回 (isQuotaExhausted, message)
// isQuotaExhausted: true 表示额度用完/Token不可用false 表示 Token 可用(可能有警告信息)
func classifyCursorRawStream(raw []byte) (isQuotaExhausted bool, message string) {
if len(raw) == 0 {
return false, ""
}
// 额度用尽只按明确完整提示判定,避免“可用但带推广/提示文案”的 Token 被误标为已用完。
// 用户自定义的二进制特征仍保留给部署方精确配置。
for _, sig := range cursorQuotaExhaustedSigsFromEnv() {
if bytes.Contains(raw, sig) {
return true, "该TOKEN已用完额度已耗尽"
}
}
if bytes.Contains(raw, cursorQuotaTipSig) {
return true, "该TOKEN已用完Get Cursor Pro for more Agent usage, unlimited Tab, and more."
}
flat := strings.ToLower(strings.ToValidUTF8(string(raw), "\uFFFD"))
flat = strings.ReplaceAll(flat, "\u2019", "'")
flat = strings.ReplaceAll(flat, "`", "'")
if strings.Contains(flat, "suspicious activity") ||
strings.Contains(flat, "unauthenticated") ||
strings.Contains(flat, "unauthorized request") ||
strings.Contains(flat, "unauthorizedrequest") ||
strings.Contains(flat, "error_unauthorized") {
return true, "该TOKEN不可用账号触发可疑活动风控/未认证,需要重新登录)"
}
// 版本过旧警告 - 这不是额度问题Token 仍然可用
// 返回 false表示 Token 可用
if strings.Contains(flat, "very old version") || strings.Contains(flat, "update to the latest version") {
return false, "Token可用但客户端版本过旧建议更新到最新版本"
}
return false, ""
}
func truncateUTF8Preview(raw []byte, maxBytes int) string {
s := strings.ToValidUTF8(string(raw), "\uFFFD")
if maxBytes <= 0 || len(s) <= maxBytes {
return s
}
// 按字节截断并保证合法 UTF-8
s = s[:maxBytes]
for len(s) > 0 && !utf8.ValidString(s) {
s = s[:len(s)-1]
}
return s + "…(已截断)"
}
func prefixHexBody(b []byte, max int) string {
if len(b) > max {
b = b[:max]
}
return hex.EncodeToString(b)
}
func looksLikeGzip(raw []byte) bool {
return len(raw) >= 3 && raw[0] == 0x1f && raw[1] == 0x8b && raw[2] == 0x08
}
func gunzipBytes(raw []byte) ([]byte, error) {
zr, err := gzip.NewReader(bytes.NewReader(raw))
if err != nil {
return nil, err
}
defer zr.Close()
return io.ReadAll(io.LimitReader(zr, cursorHiMaxRead))
}
func decodeConnectFramedBody(raw []byte) ([]byte, string, bool) {
if len(raw) < 5 {
return nil, "", false
}
var out bytes.Buffer
offset := 0
frameCount := 0
compressedFrames := 0
for offset+5 <= len(raw) {
flags := raw[offset]
n := int(raw[offset+1])<<24 | int(raw[offset+2])<<16 | int(raw[offset+3])<<8 | int(raw[offset+4])
offset += 5
if n < 0 || offset+n > len(raw) {
return nil, "", false
}
payload := raw[offset : offset+n]
offset += n
frameCount++
isCompressed := flags&0x01 == 0x01
if isCompressed || looksLikeGzip(payload) {
decoded, err := gunzipBytes(payload)
if err != nil {
out.Write(payload)
} else {
out.Write(decoded)
compressedFrames++
}
} else {
out.Write(payload)
}
}
if frameCount == 0 || offset != len(raw) {
return nil, "", false
}
return out.Bytes(), "", true
}
func decodeCursorResponseBody(raw []byte, contentEncoding string) ([]byte, string) {
if decoded, _, ok := decodeConnectFramedBody(raw); ok {
return decoded, ""
}
enc := strings.ToLower(strings.TrimSpace(contentEncoding))
if strings.Contains(enc, "gzip") || looksLikeGzip(raw) {
decoded, err := gunzipBytes(raw)
if err != nil {
return raw, ""
}
return decoded, ""
}
return raw, ""
}
// cursorStreamProtocol 与官方客户端一致Connect-RPC + protobuf 体HTTP/2 流式。
// 当前探测接口使用新版 Agent/aiserver.v1.ChatService/StreamUnifiedChatWithTools。
// 若 Cursor 后续强制更高客户端版本,可通过环境变量 CURSOR_CLIENT_VERSION 覆盖默认 X-Cursor-Client-Version。
const cursorStreamProtocol = "Connect-Protocol-Version:1 + application/connect+protoHTTP/2 二进制流gRPC/ConnectRPC 兼容形态,非 JSON REST"
// cursorStreamNote 说明 rawPreview / ok 的含义边界(与「仅通 200」结论一致
const cursorStreamNote = `【协议】本 URL 为 Cursor 官方 Agent 流式接口,请求体为 protobufrequestBodyPrefixHex 可见非表单/JSON` +
`【响应】正文为分包二进制流rawPreview 是按 UTF-8 有损解码的片段,绝大多数情况下会像乱码,属正常现象,不能当普通 UTF-8 接口正文解析。` +
`【HTTP 200】仅表示 TLS/代理/网络到 api2.cursor.sh 通畅,不代表 protobuf 业务层、鉴权、设备指纹、风控配额或「能持续对话」已全部通过。` +
`【ok 字段】当前仅在解码片段上做英文关键词启发式匹配;未命中不代表账户可用,命中也不覆盖「须在 IDE 内完整走流式协议」的场景。` +
`【若要等价客户端】需完整实现 Connect 帧解析、会话与校验头、可能的 gzip/分包及双向流,本探测只做粗连通与可观测性辅助。` +
`【二进制特征】若提示语被包在 protobuf 字段内、ASCII 子串匹配不到,可在运行环境设置 CURSOR_QUOTA_EXHAUSTED_SIG_HEX=hex1,hex2逗号分隔十六进制可选 0x 前缀),在原始响应字节上做 bytes.Contains无需解析整条 proto特征需自行对比「额度正常」与「用尽」两次抓包提取。`
// cursorQuotaExhaustedSigsFromEnv 从环境变量解析额度用尽时的二进制特征(不转 UTF-8
func cursorQuotaExhaustedSigsFromEnv() [][]byte {
s := strings.TrimSpace(os.Getenv("CURSOR_QUOTA_EXHAUSTED_SIG_HEX"))
if s == "" {
return nil
}
var out [][]byte
for _, part := range strings.Split(s, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
part = strings.TrimPrefix(strings.TrimPrefix(part, "0x"), "0X")
b, err := hex.DecodeString(part)
if err != nil || len(b) == 0 {
continue
}
out = append(out, b)
}
return out
}
func cursorProbeResult(ok bool, detail string, httpStatus int, reqBody, raw, preview []byte) Result {
if preview == nil {
preview = raw
}
return Result{
OK: ok,
Detail: detail,
HTTPStatus: httpStatus,
ProbeMessage: probeHiText,
Endpoint: cursorBackendURL + cursorAgentPath,
BytesRead: len(raw),
RawPreview: truncateUTF8Preview(preview, 24000),
RequestBodyPrefixHex: prefixHexBody(reqBody, 128),
StreamProtocol: cursorStreamProtocol,
StreamNote: cursorStreamNote,
}
}
// cursorReadableServerOutput 从 Cursor 的 protobuf 二进制流里提取适合展示的可读文本。
// 注意:这里不完整解析 proto只做展示层清洗避免把字段号、长度前缀、UUID、think 过程等内容直接展示给用户。
func cursorReadableServerOutput(decoded []byte, maxBytes int) string {
if len(decoded) == 0 {
return ""
}
s := strings.ToValidUTF8(string(decoded), "")
s = strings.ReplaceAll(s, "\uFFFD", "")
finalMarkerRe := regexp.MustCompile(`(?is)<\s*[|]\s*final\s*[|]\s*>`)
s = finalMarkerRe.ReplaceAllString(s, "<final>")
var b strings.Builder
lastSpace := false
for _, r := range s {
switch {
case r == '\r' || r == '\n' || r == '\t' || r == ' ':
if !lastSpace {
b.WriteByte('\n')
}
lastSpace = true
case r >= 32:
b.WriteRune(r)
lastSpace = false
default:
// protobuf 字段号、长度前缀等控制字符经常刚好位于单词/JSON 字段之间。
// 这里用分隔符替代直接丢弃,避免 Your + request 被粘成 Yourrequest。
if !lastSpace {
b.WriteByte('\n')
}
lastSpace = true
}
}
cleaned := strings.TrimSpace(b.String())
if cleaned == "" {
return ""
}
uuidRe := regexp.MustCompile(`(?i)\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b`)
cleaned = uuidRe.ReplaceAllString(cleaned, "")
var parts []string
const oldVersionText = "This is a very old version of Cursor. Please update to the latest version at [cursor.com/downloads](https://cursor.com/downloads)"
if strings.Contains(cleaned, oldVersionText) {
parts = append(parts, oldVersionText)
}
// 优先展示最终回复:先按 <final> 切分;没有 final 标记时,按被二进制流切碎的 </think> 标记切分。
finalText := ""
if idx := strings.LastIndex(cleaned, "<final>"); idx >= 0 {
finalText = cleaned[idx+len("<final>"):]
} else {
thinkCloseRe := regexp.MustCompile(`(?is)</\s*t\s*h\s*i\s*n\s*k\s*>`)
matches := thinkCloseRe.FindAllStringIndex(cleaned, -1)
if len(matches) > 0 {
finalText = cleaned[matches[len(matches)-1][1]:]
}
}
if strings.TrimSpace(finalText) == "" {
finalText = cleaned
}
finalText = cursorJoinFragmentedText(finalText)
if errorMessage := cursorExtractReadableCursorError(finalText); errorMessage != "" {
finalText = errorMessage
}
finalText = strings.TrimSuffix(finalText, "{}")
finalText = strings.TrimSpace(finalText)
finalText = strings.Trim(finalText, `'"#%{} `)
// 清理流尾残留的二进制标记例如a%߯B{}
tailJunkRe := regexp.MustCompile(`(?is)\s+[a-z]?%[^\s]{0,12}B\{\}\s*$`)
finalText = tailJunkRe.ReplaceAllString(finalText, "")
finalText = strings.TrimSpace(finalText)
if finalText != "" && !strings.Contains(strings.Join(parts, "\n"), finalText) {
parts = append(parts, finalText)
}
if len(parts) > 0 {
cleaned = strings.Join(parts, "\n\n")
} else {
cleaned = finalText
}
if maxBytes > 0 && len(cleaned) > maxBytes {
cleaned = cleaned[:maxBytes]
for len(cleaned) > 0 && !utf8.ValidString(cleaned) {
cleaned = cleaned[:len(cleaned)-1]
}
cleaned += "…(已截断)"
}
return strings.TrimSpace(cleaned)
}
func cursorExtractReadableCursorError(text string) string {
if text == "" {
return ""
}
unescaped := strings.ReplaceAll(text, `\n`, "\n")
unescaped = strings.ReplaceAll(unescaped, `\"`, `"`)
unescaped = strings.ReplaceAll(unescaped, `\/`, `/`)
if !(strings.Contains(strings.ToLower(unescaped), "error") ||
strings.Contains(strings.ToLower(unescaped), "unauthenticated") ||
strings.Contains(strings.ToLower(unescaped), "unauthorized") ||
strings.Contains(strings.ToLower(unescaped), "suspicious activity")) {
return ""
}
messageRe := regexp.MustCompile(`(?is)"(?:message|detail)"\s*:\s*"([^"]+)"`)
matches := messageRe.FindAllStringSubmatch(unescaped, -1)
for _, match := range matches {
if len(match) < 2 {
continue
}
msg := strings.TrimSpace(match[1])
if msg == "" {
continue
}
msg = strings.ReplaceAll(msg, `\n`, "\n")
msg = strings.ReplaceAll(msg, `\"`, `"`)
msg = cursorJoinFragmentedText(msg)
lowerMsg := strings.ToLower(msg)
if strings.Contains(lowerMsg, "suspicious activity") ||
strings.Contains(lowerMsg, "blocked") ||
strings.Contains(lowerMsg, "unauthorized") ||
strings.Contains(lowerMsg, "unauthenticated") {
return msg
}
}
if strings.Contains(strings.ToLower(unescaped), "suspicious activity") {
return "Your request has been blocked as our system has detected suspicious activity from your account. For troubleshooting, please visit the Cursor Docs at https://cursor.com/docs/troubleshooting/common-issues#suspicious-activity-message."
}
return ""
}
func cursorJoinFragmentedText(text string) string {
lines := strings.Split(text, "\n")
parts := make([]string, 0, len(lines))
for _, line := range lines {
part := strings.TrimSpace(line)
if part == "" {
continue
}
parts = append(parts, part)
}
out := strings.Join(parts, " ")
// 标点前不保留空格。
punctRe := regexp.MustCompile(`\s+([.,!?;:)\]}"',。!?;:)】》])`)
out = punctRe.ReplaceAllString(out, "$1")
// 只修复很明确的“单词内部被切开”场景,避免把 How can / I help / with your 误拼成 Howcan / Ihelp / withyour。
singlePrefixRe := regexp.MustCompile(`\b([b-hj-zB-HJ-Z])\s+([a-z]{2,})\b`)
out = singlePrefixRe.ReplaceAllString(out, "$1$2")
commonSuffixRe := regexp.MustCompile(`\b([A-Za-z]{3,})\s+(ing|ed|er|ers|ly|s)\b`)
out = commonSuffixRe.ReplaceAllString(out, "$1$2")
spaceRe := regexp.MustCompile(`\s+`)
out = spaceRe.ReplaceAllString(out, " ")
return strings.TrimSpace(out)
}
// cursorServerOutputDetail 将服务器响应内容放入 detail便于前端只展示 detail 时也能看到服务端输出。
func cursorServerOutputDetail(prefix string, decoded []byte) string {
serverOutput := cursorReadableServerOutput(decoded, 8000)
if serverOutput == "" {
return prefix
}
return prefix + ",服务器可读输出:\n" + serverOutput
}
// probeCursorHiAgent 探测 Cursor Token 可用性
func probeCursorHiAgent(authToken string) Result {
if strings.Contains(authToken, "::") {
if i := strings.LastIndex(authToken, "::"); i >= 0 {
authToken = strings.TrimSpace(authToken[i+2:])
}
}
if authToken == "" {
return Result{OK: false, Detail: "Token 为空"}
}
sessionID := uuid.NewSHA1(uuid.NameSpaceDNS, []byte(authToken)).String()
clientKey := hashed64Hex(authToken, "")
checksum := generateCursorChecksum(authToken)
conversationID := uuid.NewString()
reqID := uuid.NewString()
body := generateCursorAgentFramedBody(probeHiText, "default")
fullURL := cursorBackendURL + cursorAgentPath
req, err := http.NewRequest(http.MethodPost, fullURL, bytes.NewReader(body))
if err != nil {
r := cursorProbeResult(false, "请求失败: "+err.Error(), 0, body, nil, nil)
return r
}
req.Header.Set("Authorization", "Bearer "+authToken)
req.Header.Set("Connect-Accept-Encoding", "gzip")
req.Header.Set("Connect-Protocol-Version", "1")
req.Header.Set("Content-Type", "application/connect+proto")
req.Header.Set("User-Agent", "connect-es/1.6.1")
req.Header.Set("X-Amzn-Trace-Id", "Root="+reqID)
req.Header.Set("X-Client-Key", clientKey)
req.Header.Set("X-Cursor-Checksum", checksum)
req.Header.Set("X-Cursor-Client-Version", cursorEnvVersion())
req.Header.Set("X-Cursor-Client-Type", "ide")
req.Header.Set("X-Cursor-Client-Os", cursorClientOS())
req.Header.Set("X-Cursor-Client-Arch", cursorClientArch())
req.Header.Set("X-Cursor-Client-Os-Version", "unknown")
req.Header.Set("X-Cursor-Client-Device-Type", "desktop")
req.Header.Set("X-Cursor-Config-Version", uuid.NewString())
req.Header.Set("X-Cursor-Timezone", "UTC")
req.Header.Set("X-Ghost-Mode", "false")
req.Header.Set("X-New-Onboarding-Completed", "true")
req.Header.Set("X-Request-Id", reqID)
req.Header.Set("X-Session-Id", sessionID)
req.Header.Set("X-Conversation-Id", conversationID)
req.Host = "api2.cursor.sh"
resp, err := cursorProbeHTTPClient.Do(req)
if err != nil {
return cursorProbeResult(false, "请求失败: "+err.Error(), 0, body, nil, nil)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
raw, _ := io.ReadAll(io.LimitReader(resp.Body, cursorHiMaxRead))
decoded, _ := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
isQuotaExhausted, msg := classifyCursorRawStream(decoded)
if isQuotaExhausted {
return cursorProbeResult(false, msg, resp.StatusCode, body, raw, decoded)
}
// 非 200 状态码且不是额度问题
return cursorProbeResult(false, fmt.Sprintf("HTTP %d - Token不可用", resp.StatusCode), resp.StatusCode, body, raw, decoded)
}
var buf bytes.Buffer
_, _ = io.Copy(&buf, io.LimitReader(resp.Body, cursorHiMaxRead))
raw := buf.Bytes()
decoded, _ := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
isQuotaExhausted, msg := classifyCursorRawStream(decoded)
if isQuotaExhausted {
// Token 不可用(额度用完等)
return cursorProbeResult(false, msg, resp.StatusCode, body, raw, decoded)
}
// Token 可用时也把服务器实际输出放到 Detail避免前端只展示 Detail 时看不到 RawPreview。
if msg != "" {
return cursorProbeResult(true, cursorServerOutputDetail(msg, decoded), resp.StatusCode, body, raw, decoded)
}
return cursorProbeResult(true, cursorServerOutputDetail("Token可用", decoded), resp.StatusCode, body, raw, decoded)
}