更新检测流程
This commit is contained in:
+240
-73
@@ -10,6 +10,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -22,7 +23,7 @@ import (
|
||||
const (
|
||||
cursorBackendURL = "https://api2.cursor.sh"
|
||||
cursorAgentPath = "/aiserver.v1.ChatService/StreamUnifiedChatWithTools"
|
||||
cursorClientVersion = "2.6.22"
|
||||
cursorClientVersion = "3.6.31"
|
||||
cursorHiMaxRead = 512 * 1024
|
||||
// probeHiText 发往官方 Agent 的探测内容(与前端展示 probeMessage 一致)
|
||||
probeHiText = "hi"
|
||||
@@ -276,55 +277,43 @@ var cursorQuotaTipSig = []byte("Get Cursor Pro for more Agent usage, unlimited T
|
||||
|
||||
const cursorLimitTipPrefix = "Get Cursor Pro for more Agent usage, unlimited Tab"
|
||||
|
||||
// classifyCursorRawStream 在官方流式二进制/文本中匹配用量与升级提示(ASCII 区不区分大小写 + UTF-8 短语)
|
||||
func classifyCursorRawStream(raw []byte) (blocked bool, reason string) {
|
||||
// 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, fmt.Sprintf("流中匹配:CURSOR_QUOTA_EXHAUSTED_SIG_HEX 配置的二进制特征(%d 字节)", len(sig))
|
||||
return true, "该TOKEN已用完(额度已耗尽)"
|
||||
}
|
||||
}
|
||||
if bytes.Contains(raw, cursorQuotaTipSig) {
|
||||
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"
|
||||
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, "\u2019", "'")
|
||||
flat = strings.ReplaceAll(flat, "`", "'")
|
||||
if strings.Contains(flat, "you've hit your usage limit") {
|
||||
return true, "流中匹配:you've hit your usage limit(UTF-8)"
|
||||
|
||||
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, ""
|
||||
}
|
||||
|
||||
@@ -400,41 +389,29 @@ func decodeConnectFramedBody(raw []byte) ([]byte, string, bool) {
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
note := fmt.Sprintf("响应体已按 Connect 分帧解析(%d 帧", frameCount)
|
||||
if compressedFrames > 0 {
|
||||
note += fmt.Sprintf(",其中 %d 帧已做 gzip 解压", compressedFrames)
|
||||
}
|
||||
note += ")后分析"
|
||||
return out.Bytes(), note, true
|
||||
return out.Bytes(), "", true
|
||||
}
|
||||
|
||||
func decodeCursorResponseBody(raw []byte, contentEncoding string) ([]byte, string) {
|
||||
if decoded, note, ok := decodeConnectFramedBody(raw); ok {
|
||||
return decoded, note
|
||||
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 {
|
||||
if strings.Contains(enc, "gzip") {
|
||||
return raw, "响应头声明 gzip,但解压失败,已回退为原始字节预览"
|
||||
}
|
||||
return raw, "检测到 gzip 魔数,但解压失败,已回退为原始字节预览"
|
||||
return raw, ""
|
||||
}
|
||||
if strings.Contains(enc, "gzip") {
|
||||
return decoded, "响应体已按 gzip 解压后分析"
|
||||
}
|
||||
return decoded, "响应体虽未显式声明 Content-Encoding,但按 gzip 魔数解压后分析"
|
||||
return decoded, ""
|
||||
}
|
||||
if enc != "" {
|
||||
return raw, "响应头 Content-Encoding=" + enc + ",当前未额外解码,按原始字节分析"
|
||||
}
|
||||
return raw, "响应体未压缩或未声明压缩,且未识别为 Connect 分帧,按原始字节分析"
|
||||
return raw, ""
|
||||
}
|
||||
|
||||
// cursorStreamProtocol 与官方客户端一致:Connect-RPC + protobuf 体,HTTP/2 流式
|
||||
const cursorStreamProtocol = "Connect-Protocol-Version:1 + application/connect+proto,HTTP/2 二进制流(gRPC 兼容形态,非 JSON REST)"
|
||||
// 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)。` +
|
||||
@@ -484,6 +461,189 @@ 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 {
|
||||
@@ -504,7 +664,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)
|
||||
@@ -532,29 +692,36 @@ func probeCursorHiAgent(authToken string) Result {
|
||||
|
||||
resp, err := cursorProbeHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return cursorProbeResult(false, "请求 Cursor Agent 失败: "+err.Error(), 0, body, nil, 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, decodeNote := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||
blocked, reason := classifyCursorRawStream(decoded)
|
||||
if blocked {
|
||||
return cursorProbeResult(false, reason+";"+decodeNote, resp.StatusCode, body, raw, decoded)
|
||||
decoded, _ := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||
isQuotaExhausted, msg := classifyCursorRawStream(decoded)
|
||||
if isQuotaExhausted {
|
||||
return cursorProbeResult(false, msg, 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)
|
||||
// 非 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, decodeNote := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||
blocked, reason := classifyCursorRawStream(decoded)
|
||||
if blocked {
|
||||
return cursorProbeResult(false, reason+";"+decodeNote, resp.StatusCode, body, raw, decoded)
|
||||
decoded, _ := decodeCursorResponseBody(raw, resp.Header.Get("Content-Encoding"))
|
||||
isQuotaExhausted, msg := classifyCursorRawStream(decoded)
|
||||
|
||||
if isQuotaExhausted {
|
||||
// Token 不可用(额度用完等)
|
||||
return cursorProbeResult(false, msg, resp.StatusCode, body, raw, decoded)
|
||||
}
|
||||
detail := "HTTP 200;未命中内置英文关键词;" + decodeNote + ";二进制流含义与 ok 边界见 streamNote"
|
||||
return cursorProbeResult(true, detail, 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)
|
||||
}
|
||||
|
||||
+16
-10
@@ -26,9 +26,8 @@ type Result struct {
|
||||
BytesRead int `json:"bytesRead,omitempty"`
|
||||
RawPreview string `json:"rawPreview,omitempty"`
|
||||
RequestBodyPrefixHex string `json:"requestBodyPrefixHex,omitempty"`
|
||||
// StreamProtocol / StreamNote 仅 Cursor Agent 探测填充,说明二进制流与结论边界
|
||||
StreamProtocol string `json:"streamProtocol,omitempty"`
|
||||
StreamNote string `json:"streamNote,omitempty"`
|
||||
StreamProtocol string `json:"streamProtocol,omitempty"`
|
||||
StreamNote string `json:"streamNote,omitempty"`
|
||||
}
|
||||
|
||||
// ProbeOfficial 按号池模块探测 Token(cursor / windsurf / krio)
|
||||
@@ -39,7 +38,8 @@ func ProbeOfficial(module, rawToken string) Result {
|
||||
}
|
||||
switch module {
|
||||
case "cursor":
|
||||
return probeCursor(tok)
|
||||
// 直接使用 cursor_hi.go 中已有的完整探测函数
|
||||
return probeCursorHiAgent(tok)
|
||||
case "windsurf":
|
||||
return probeWindsurf(tok)
|
||||
case "krio":
|
||||
@@ -57,19 +57,21 @@ 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{
|
||||
"apiKey": apiKey,
|
||||
"ideName": "windsurf",
|
||||
"ideVersion": "0.0.0",
|
||||
"extensionName": "windsurf",
|
||||
"extensionVersion": "0.0.0",
|
||||
"locale": "zh",
|
||||
"apiKey": apiKey,
|
||||
"ideName": "windsurf",
|
||||
"ideVersion": "0.0.0",
|
||||
"extensionName": "windsurf",
|
||||
"extensionVersion": "0.0.0",
|
||||
"locale": "zh",
|
||||
},
|
||||
}
|
||||
raw, err := json.Marshal(payload)
|
||||
@@ -117,6 +119,7 @@ func probeWindsurf(apiKey string) Result {
|
||||
}
|
||||
}
|
||||
|
||||
// probeKiro Kiro 探测
|
||||
func probeKiro(accessToken string) Result {
|
||||
arn := findProfileArnInJWT(accessToken)
|
||||
if arn == "" {
|
||||
@@ -160,6 +163,7 @@ 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, ".")
|
||||
@@ -177,6 +181,7 @@ 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 {
|
||||
@@ -185,6 +190,7 @@ func findProfileArnInJWT(raw string) string {
|
||||
return findProfileArnValue(m)
|
||||
}
|
||||
|
||||
// findProfileArnValue 递归查找 profileArn
|
||||
func findProfileArnValue(v interface{}) string {
|
||||
switch x := v.(type) {
|
||||
case map[string]interface{}:
|
||||
|
||||
Reference in New Issue
Block a user