整合数据

This commit is contained in:
2026-06-16 01:30:39 +08:00
parent 761a5cb69c
commit c0f70823a9
31 changed files with 4385 additions and 893 deletions
+73 -240
View File
@@ -10,7 +10,6 @@ import (
"io"
"net/http"
"os"
"regexp"
"runtime"
"strings"
"time"
@@ -23,7 +22,7 @@ import (
const (
cursorBackendURL = "https://api2.cursor.sh"
cursorAgentPath = "/aiserver.v1.ChatService/StreamUnifiedChatWithTools"
cursorClientVersion = "3.6.31"
cursorClientVersion = "2.6.22"
cursorHiMaxRead = 512 * 1024
// probeHiText 发往官方 Agent 的探测内容(与前端展示 probeMessage 一致)
probeHiText = "hi"
@@ -277,43 +276,55 @@ var cursorQuotaTipSig = []byte("Get Cursor Pro for more Agent usage, unlimited T
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) {
// classifyCursorRawStream 在官方流式二进制/文本中匹配用量与升级提示ASCII 区不区分大小写 + UTF-8 短语)
func classifyCursorRawStream(raw []byte) (blocked bool, reason string) {
if len(raw) == 0 {
return false, ""
}
// 额度用尽只按明确完整提示判定,避免“可用但带推广/提示文案”的 Token 被误标为已用完。
// 用户自定义的二进制特征仍保留给部署方精确配置。
for _, sig := range cursorQuotaExhaustedSigsFromEnv() {
if bytes.Contains(raw, sig) {
return true, "该TOKEN已用完(额度已耗尽)"
return true, fmt.Sprintf("流中匹配:CURSOR_QUOTA_EXHAUSTED_SIG_HEX 配置的二进制特征(%d 字节)", len(sig))
}
}
if bytes.Contains(raw, cursorQuotaTipSig) {
return true, "该TOKEN已用完(Get Cursor Pro for more Agent usage, unlimited Tab, and more."
return true, "流中匹配:" + string(cursorQuotaTipSig)
}
// 社区脚本:仅到「…Agent usage」的 ASCII 前缀(流里可能只有前半段)
if bytes.Contains(raw, cursorQuotaExhaustedSigCommunity) {
return true, "流中匹配:Get Cursor Pro for more Agent usage…(社区 QuotaExhaustedSignature 前缀)"
}
if bytes.Contains(raw, []byte(cursorLimitTipPrefix)) {
return true, "流中匹配:" + cursorLimitTipPrefix + "…"
}
low := append([]byte(nil), raw...)
asciiLowerInPlace(low)
if bytes.Contains(low, []byte("you've hit your usage limit")) ||
bytes.Contains(low, []byte("youve hit your usage limit")) ||
bytes.Contains(low, []byte("hit your usage limit")) {
return true, "流中匹配:hit your usage limit / you've hit your usage limit"
}
if bytes.Contains(low, []byte("get cursor pro for more agent usage")) {
return true, "流中匹配:get cursor pro for more agent usage"
}
if bytes.Contains(low, []byte("upgrade to pro")) {
return true, "流中匹配:upgrade to pro"
}
if bytes.Contains(low, []byte("get cursor pro")) && bytes.Contains(low, []byte("agent")) {
return true, "流中匹配:get cursor pro + agent"
}
if bytes.Contains(low, []byte("usage limit")) {
return true, "流中匹配:usage limit"
}
if bytes.Contains(low, []byte("unlimited tab")) && bytes.Contains(low, []byte("cursor pro")) {
return true, "流中匹配:unlimited tab + cursor pro"
}
flat := strings.ToLower(strings.ToValidUTF8(string(raw), "\uFFFD"))
flat = strings.ReplaceAll(flat, "\u2019", "'")
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不可用(账号触发可疑活动风控/未认证,需要重新登录)"
if strings.Contains(flat, "you've hit your usage limit") {
return true, "流中匹配:you've hit your usage limitUTF-8"
}
// 版本过旧警告 - 这不是额度问题,Token 仍然可用
// 返回 false,表示 Token 可用
if strings.Contains(flat, "very old version") || strings.Contains(flat, "update to the latest version") {
return false, "Token可用,但客户端版本过旧,建议更新到最新版本"
}
return false, ""
}
@@ -389,29 +400,41 @@ func decodeConnectFramedBody(raw []byte) ([]byte, string, bool) {
return nil, "", false
}
return out.Bytes(), "", true
note := fmt.Sprintf("响应体已按 Connect 分帧解析(%d 帧", frameCount)
if compressedFrames > 0 {
note += fmt.Sprintf(",其中 %d 帧已做 gzip 解压", compressedFrames)
}
note += ")后分析"
return out.Bytes(), note, true
}
func decodeCursorResponseBody(raw []byte, contentEncoding string) ([]byte, string) {
if decoded, _, ok := decodeConnectFramedBody(raw); ok {
return decoded, ""
if decoded, note, ok := decodeConnectFramedBody(raw); ok {
return decoded, note
}
enc := strings.ToLower(strings.TrimSpace(contentEncoding))
if strings.Contains(enc, "gzip") || looksLikeGzip(raw) {
decoded, err := gunzipBytes(raw)
if err != nil {
return raw, ""
if strings.Contains(enc, "gzip") {
return raw, "响应头声明 gzip,但解压失败,已回退为原始字节预览"
}
return raw, "检测到 gzip 魔数,但解压失败,已回退为原始字节预览"
}
return decoded, ""
if strings.Contains(enc, "gzip") {
return decoded, "响应体已按 gzip 解压后分析"
}
return decoded, "响应体虽未显式声明 Content-Encoding,但按 gzip 魔数解压后分析"
}
return raw, ""
if enc != "" {
return raw, "响应头 Content-Encoding=" + enc + ",当前未额外解码,按原始字节分析"
}
return raw, "响应体未压缩或未声明压缩,且未识别为 Connect 分帧,按原始字节分析"
}
// 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"
// cursorStreamProtocol 与官方客户端一致:Connect-RPC + protobuf 体,HTTP/2 流式
const cursorStreamProtocol = "Connect-Protocol-Version:1 + application/connect+protoHTTP/2 二进制流(gRPC 兼容形态,非 JSON REST"
// cursorStreamNote 说明 rawPreview / ok 的含义边界(与「仅通 200」结论一致)
const cursorStreamNote = `【协议】本 URL 为 Cursor 官方 Agent 流式接口,请求体为 protobufrequestBodyPrefixHex 可见非表单/JSON)。` +
@@ -461,189 +484,6 @@ func cursorProbeResult(ok bool, detail string, httpStatus int, reqBody, raw, pre
}
}
// 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 {
@@ -664,7 +504,7 @@ func probeCursorHiAgent(authToken string) Result {
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)
r := cursorProbeResult(false, err.Error(), 0, body, nil, nil)
return r
}
req.Header.Set("Authorization", "Bearer "+authToken)
@@ -692,36 +532,29 @@ func probeCursorHiAgent(authToken string) Result {
resp, err := cursorProbeHTTPClient.Do(req)
if err != nil {
return cursorProbeResult(false, "请求失败: "+err.Error(), 0, body, nil, nil)
return cursorProbeResult(false, "请求 Cursor Agent 失败: "+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)
decoded, decodeNote := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
blocked, reason := classifyCursorRawStream(decoded)
if blocked {
return cursorProbeResult(false, reason+""+decodeNote, resp.StatusCode, body, raw, decoded)
}
// 非 200 状态码且不是额度问题
return cursorProbeResult(false, fmt.Sprintf("HTTP %d - Token不可用", resp.StatusCode), resp.StatusCode, body, raw, decoded)
detail := fmt.Sprintf("HTTP %d(非 200);%s;说明与协议边界见 streamNote", resp.StatusCode, decodeNote)
return cursorProbeResult(false, detail, 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)
decoded, decodeNote := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
blocked, reason := classifyCursorRawStream(decoded)
if blocked {
return cursorProbeResult(false, reason+""+decodeNote, 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)
detail := "HTTP 200;未命中内置英文关键词;" + decodeNote + ";二进制流含义与 ok 边界见 streamNote"
return cursorProbeResult(true, detail, resp.StatusCode, body, raw, decoded)
}
+9 -14
View File
@@ -1,9 +1,8 @@
// Package tokenprobe 使用号池内 Token 调用各厂商接口做可用性探测。
// Cursor 走 api2.cursor.sh 的 Connect + protobuf 二进制流(非 JSON 文本接口)。
package tokenprobe
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
@@ -14,9 +13,13 @@ import (
"time"
)
var httpClient = &http.Client{Timeout: 25 * time.Second}
var httpClient = &http.Client{
Timeout: 12 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
// Result 探测结果(Cursor 会填充 ProbeMessage / Endpoint / BytesRead / RawPreview 等)
type Result struct {
OK bool `json:"ok"`
Detail string `json:"detail"`
@@ -30,7 +33,6 @@ type Result struct {
StreamNote string `json:"streamNote,omitempty"`
}
// ProbeOfficial 按号池模块探测 Tokencursor / windsurf / krio
func ProbeOfficial(module, rawToken string) Result {
tok := normalizeBearerToken(strings.TrimSpace(rawToken))
if tok == "" {
@@ -38,8 +40,7 @@ func ProbeOfficial(module, rawToken string) Result {
}
switch module {
case "cursor":
// 直接使用 cursor_hi.go 中已有的完整探测函数
return probeCursorHiAgent(tok)
return probeCursor(tok)
case "windsurf":
return probeWindsurf(tok)
case "krio":
@@ -57,12 +58,10 @@ func normalizeBearerToken(s string) string {
return s
}
// probeCursor Cursor Token 探测(直接使用 cursor_hi.go 的实现)
func probeCursor(token string) Result {
return probeCursorHiAgent(token)
}
// probeWindsurf WindSurf 探测
func probeWindsurf(apiKey string) Result {
payload := map[string]interface{}{
"metadata": map[string]string{
@@ -119,13 +118,12 @@ func probeWindsurf(apiKey string) Result {
}
}
// probeKiro Kiro 探测
func probeKiro(accessToken string) Result {
arn := findProfileArnInJWT(accessToken)
if arn == "" {
return Result{
OK: false,
Detail: "无法从 Token 中解析 profileArnKiro 暂无法自动探测(需完整登录 JWT",
Detail: "无法从 Token 中解析 profileArnKiro 暂无法自动探测",
}
}
@@ -163,7 +161,6 @@ func probeKiro(accessToken string) Result {
}
}
// decodeJWTPayloadMap 解析 JWT payload
func decodeJWTPayloadMap(raw string) (map[string]interface{}, error) {
tok := normalizeBearerToken(strings.TrimSpace(raw))
parts := strings.Split(tok, ".")
@@ -181,7 +178,6 @@ func decodeJWTPayloadMap(raw string) (map[string]interface{}, error) {
return m, nil
}
// findProfileArnInJWT 从 JWT 中查找 profileArn
func findProfileArnInJWT(raw string) string {
m, err := decodeJWTPayloadMap(raw)
if err != nil {
@@ -190,7 +186,6 @@ func findProfileArnInJWT(raw string) string {
return findProfileArnValue(m)
}
// findProfileArnValue 递归查找 profileArn
func findProfileArnValue(v interface{}) string {
switch x := v.(type) {
case map[string]interface{}: