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+proto,HTTP/2 二进制流(gRPC/ConnectRPC 兼容形态,非 JSON REST)" // cursorStreamNote 说明 rawPreview / ok 的含义边界(与「仅通 200」结论一致) const cursorStreamNote = `【协议】本 URL 为 Cursor 官方 Agent 流式接口,请求体为 protobuf(requestBodyPrefixHex 可见非表单/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, "") 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 标记时,按被二进制流切碎的 标记切分。 finalText := "" if idx := strings.LastIndex(cleaned, ""); idx >= 0 { finalText = cleaned[idx+len(""):] } else { thinkCloseRe := regexp.MustCompile(`(?is)`) 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) }