go-platform/pkg/tokenprobe/probe.go
2026-05-05 23:54:15 +08:00

217 lines
6.4 KiB
Go
Raw Permalink 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 使用号池内 Token 调用各厂商接口做可用性探测。
// Cursor 走 api2.cursor.sh 的 Connect + protobuf 二进制流(非 JSON 文本接口)。
package tokenprobe
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
var httpClient = &http.Client{Timeout: 25 * time.Second}
// Result 探测结果Cursor 会填充 ProbeMessage / Endpoint / BytesRead / RawPreview 等)
type Result struct {
OK bool `json:"ok"`
Detail string `json:"detail"`
HTTPStatus int `json:"httpStatus"`
ProbeMessage string `json:"probeMessage,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
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"`
}
// ProbeOfficial 按号池模块探测 Tokencursor / windsurf / krio
func ProbeOfficial(module, rawToken string) Result {
tok := normalizeBearerToken(strings.TrimSpace(rawToken))
if tok == "" {
return Result{OK: false, Detail: "Token 为空"}
}
switch module {
case "cursor":
return probeCursor(tok)
case "windsurf":
return probeWindsurf(tok)
case "krio":
return probeKiro(tok)
default:
return Result{OK: false, Detail: "未知模块"}
}
}
func normalizeBearerToken(s string) string {
s = strings.TrimSpace(s)
if i := strings.LastIndex(s, "::"); i >= 0 {
return strings.TrimSpace(s[i+2:])
}
return s
}
func probeCursor(token string) Result {
return probeCursorHiAgent(token)
}
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",
},
}
raw, err := json.Marshal(payload)
if err != nil {
return Result{OK: false, Detail: err.Error()}
}
req, err := http.NewRequest(
http.MethodPost,
"https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetUserStatus",
bytes.NewReader(raw),
)
if err != nil {
return Result{OK: false, Detail: err.Error()}
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Connect-Protocol-Version", "1")
resp, err := httpClient.Do(req)
if err != nil {
return Result{OK: false, Detail: "请求失败: " + err.Error()}
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
switch resp.StatusCode {
case http.StatusOK:
var wrap map[string]interface{}
if json.Unmarshal(body, &wrap) == nil {
if _, ok := wrap["userStatus"]; ok {
return Result{OK: true, Detail: "Codeium 云端接口响应正常", HTTPStatus: resp.StatusCode}
}
}
if bytes.Contains(body, []byte(`"planStatus"`)) || bytes.Contains(body, []byte(`"userStatus"`)) {
return Result{OK: true, Detail: "Codeium 云端接口响应正常", HTTPStatus: resp.StatusCode}
}
return Result{OK: true, Detail: fmt.Sprintf("HTTP %d已收到响应", resp.StatusCode), HTTPStatus: resp.StatusCode}
case http.StatusUnauthorized, http.StatusForbidden:
return Result{OK: false, Detail: fmt.Sprintf("API Key 无效或已失效HTTP %d", resp.StatusCode), HTTPStatus: resp.StatusCode}
default:
snip := strings.TrimSpace(string(body))
if len(snip) > 220 {
snip = snip[:220] + "…"
}
return Result{OK: false, Detail: fmt.Sprintf("HTTP %d %s", resp.StatusCode, snip), HTTPStatus: resp.StatusCode}
}
}
func probeKiro(accessToken string) Result {
arn := findProfileArnInJWT(accessToken)
if arn == "" {
return Result{
OK: false,
Detail: "无法从 Token 中解析 profileArnKiro 暂无法自动探测(需完整登录 JWT",
}
}
q := url.Values{}
q.Set("origin", "AI_EDITOR")
q.Set("profileArn", arn)
q.Set("resourceType", "AGENTIC_REQUEST")
u := "https://q.us-east-1.amazonaws.com/getUsageLimits?" + q.Encode()
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return Result{OK: false, Detail: err.Error()}
}
req.Header.Set("Authorization", "Bearer "+normalizeBearerToken(accessToken))
req.Header.Set("Accept", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return Result{OK: false, Detail: "请求失败: " + err.Error()}
}
defer resp.Body.Close()
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
switch resp.StatusCode {
case http.StatusOK:
return Result{OK: true, Detail: "KiroAWS Q用量接口响应正常", HTTPStatus: resp.StatusCode}
case http.StatusUnauthorized, http.StatusForbidden:
return Result{OK: false, Detail: fmt.Sprintf("Token 无效或已过期HTTP %d", resp.StatusCode), HTTPStatus: resp.StatusCode}
default:
snip := strings.TrimSpace(string(body))
if len(snip) > 220 {
snip = snip[:220] + "…"
}
return Result{OK: false, Detail: fmt.Sprintf("HTTP %d %s", resp.StatusCode, snip), HTTPStatus: resp.StatusCode}
}
}
func decodeJWTPayloadMap(raw string) (map[string]interface{}, error) {
tok := normalizeBearerToken(strings.TrimSpace(raw))
parts := strings.Split(tok, ".")
if len(parts) < 2 {
return nil, fmt.Errorf("not a JWT")
}
b, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, err
}
var m map[string]interface{}
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
}
return m, nil
}
func findProfileArnInJWT(raw string) string {
m, err := decodeJWTPayloadMap(raw)
if err != nil {
return ""
}
return findProfileArnValue(m)
}
func findProfileArnValue(v interface{}) string {
switch x := v.(type) {
case map[string]interface{}:
for k, val := range x {
lk := strings.ToLower(k)
if lk == "profilearn" || lk == "profile_arn" {
if s, ok := val.(string); ok && strings.Contains(s, "arn:") {
return s
}
}
}
for _, val := range x {
if s := findProfileArnValue(val); s != "" {
return s
}
}
case []interface{}:
for _, el := range x {
if s := findProfileArnValue(el); s != "" {
return s
}
}
case string:
if strings.Contains(x, "arn:aws:codewhisperer") && strings.Contains(x, ":profile/") {
return x
}
}
return ""
}