From e845651933609f8b3cc40b08f80d940c54dc4f1f Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 23 Sep 2026 14:56:46 +0800 Subject: [PATCH] fix(yeeke): read captcha key from randomImage "key" field (#336) The live /sys/randomImage response names the captcha key "key"; the client only read "checkKey", so it always sent an empty checkKey, the login call returned "fields required" before reaching yeeke, and every OCR attempt was reported as a captcha failure. Read "key" (falling back to "checkKey"), fail explicitly when neither is present, and send remember_me like the web client does. Test fixtures used the wrong shape, which is why the bug was not caught; they now match the HAR. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F --- server/app/goauto/yeekeclient/client.go | 15 ++++- server/app/goauto/yeekeclient/client_test.go | 61 ++++++++++++++++++- server/app/goauto/yeekeclient/connect_test.go | 2 +- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/server/app/goauto/yeekeclient/client.go b/server/app/goauto/yeekeclient/client.go index 8a15a44..7cd9661 100644 --- a/server/app/goauto/yeekeclient/client.go +++ b/server/app/goauto/yeekeclient/client.go @@ -148,13 +148,24 @@ func (c *Client) FetchCaptcha(ctx context.Context) (*Captcha, error) { if e != nil { return nil, e } + // The live randomImage response carries the captcha key as "key" (seen in + // the HAR evidence for #336); "checkKey" is only the name the login body + // uses. Accept both so a future rename on either side still works. var p struct { Image string `json:"image"` + Key string `json:"key"` CheckKey string `json:"checkKey"` } if e = json.Unmarshal(raw, &p); e != nil { return nil, e } + key := p.Key + if key == "" { + key = p.CheckKey + } + if key == "" { + return nil, fmt.Errorf("yeeke captcha response missing key") + } s := p.Image if i := strings.Index(s, ","); i >= 0 { s = s[i+1:] @@ -163,7 +174,7 @@ func (c *Client) FetchCaptcha(ctx context.Context) (*Captcha, error) { if e != nil { return nil, e } - return &Captcha{Image: img, CheckKey: p.CheckKey, ContentType: "image/jpeg"}, nil + return &Captcha{Image: img, CheckKey: key, ContentType: "image/jpeg"}, nil } type LoginResult struct { @@ -208,7 +219,7 @@ func (c *Client) Login(ctx context.Context, username, password, captcha, checkKe if username == "" || password == "" || captcha == "" || checkKey == "" { return nil, fmt.Errorf("login fields required") } - raw, e := c.do(ctx, http.MethodPost, "/agent-foreign/sys/login", map[string]string{"username": username, "password": password, "captcha": captcha, "checkKey": checkKey, "agentCode": "mmt"}, nil) + raw, e := c.do(ctx, http.MethodPost, "/agent-foreign/sys/login", map[string]any{"username": username, "password": password, "captcha": captcha, "checkKey": checkKey, "agentCode": "mmt", "remember_me": true}, nil) if e != nil { return nil, e } diff --git a/server/app/goauto/yeekeclient/client_test.go b/server/app/goauto/yeekeclient/client_test.go index 15af987..99bb0bb 100644 --- a/server/app/goauto/yeekeclient/client_test.go +++ b/server/app/goauto/yeekeclient/client_test.go @@ -13,7 +13,7 @@ func TestCaptchaLoginAndReadOnlyList(t *testing.T) { w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/agent-foreign/sys/randomImage": - json.NewEncoder(w).Encode(map[string]any{"success": true, "result": map[string]string{"image": "data:image/jpg;base64,SGk=", "checkKey": "k"}}) + json.NewEncoder(w).Encode(map[string]any{"success": true, "result": map[string]string{"image": "data:image/jpg;base64,SGk=", "key": "k"}}) case "/agent-foreign/sys/login": json.NewEncoder(w).Encode(map[string]any{"success": true, "result": map[string]any{"token": "opaque", "userInfo": map[string]any{"id": "u"}}}) case "/agent-foreign/packageClaimRec/relation/list": @@ -38,3 +38,62 @@ func TestCaptchaLoginAndReadOnlyList(t *testing.T) { t.Fatal(e) } } + +// The live randomImage response names the captcha key "key" (HAR evidence for +// #336). A client that only read "checkKey" sent an empty checkKey, never +// reached the login endpoint, and every OCR attempt "failed". This pins the +// real response shape end to end: the key from randomImage must arrive as the +// login body's checkKey, together with remember_me. +func TestLoginWithOCRSendsCaptchaKeyFromRandomImage(t *testing.T) { + var loginBody map[string]any + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/agent-foreign/sys/randomImage": + json.NewEncoder(w).Encode(map[string]any{"success": true, "code": 0, "result": map[string]string{"image": "data:image/jpg;base64,SGk=", "key": "0123456789abcdef0123456789abcdef"}}) + case "/agent-foreign/sys/login": + _ = json.NewDecoder(r.Body).Decode(&loginBody) + json.NewEncoder(w).Encode(map[string]any{"success": true, "code": 200, "result": map[string]any{"token": "opaque", "userInfo": map[string]any{"id": "u"}}}) + default: + http.NotFound(w, r) + } + })) + defer s.Close() + c, _ := New(s.URL) + if _, e := c.LoginWithOCR(context.Background(), fixedOCR("abcd"), "u", "p", 3); e != nil { + t.Fatalf("login: %v", e) + } + if loginBody == nil { + t.Fatal("login endpoint was never called") + } + if loginBody["checkKey"] != "0123456789abcdef0123456789abcdef" { + t.Fatalf("checkKey = %v, want the randomImage key", loginBody["checkKey"]) + } + if loginBody["remember_me"] != true || loginBody["captcha"] != "abcd" || loginBody["agentCode"] != "mmt" { + t.Fatalf("unexpected login body shape: %v", keysOnly(loginBody)) + } +} + +func TestFetchCaptchaRejectsResponseWithoutKey(t *testing.T) { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]any{"success": true, "result": map[string]string{"image": "data:image/jpg;base64,SGk="}}) + })) + defer s.Close() + c, _ := New(s.URL) + if _, e := c.FetchCaptcha(context.Background()); e == nil { + t.Fatal("expected an explicit error when the captcha key is missing") + } +} + +type fixedOCR string + +func (f fixedOCR) Recognize(context.Context, []byte) (string, error) { return string(f), nil } + +func keysOnly(m map[string]any) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + return out +} diff --git a/server/app/goauto/yeekeclient/connect_test.go b/server/app/goauto/yeekeclient/connect_test.go index 5362396..82f18d5 100644 --- a/server/app/goauto/yeekeclient/connect_test.go +++ b/server/app/goauto/yeekeclient/connect_test.go @@ -47,7 +47,7 @@ func fakeServer(t *testing.T, loginOK func(captcha string) bool, sessionValid fu w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/agent-foreign/sys/randomImage": - json.NewEncoder(w).Encode(map[string]any{"success": true, "result": map[string]string{"image": "data:image/jpg;base64,SGk=", "checkKey": "k"}}) + json.NewEncoder(w).Encode(map[string]any{"success": true, "result": map[string]string{"image": "data:image/jpg;base64,SGk=", "key": "k"}}) case "/agent-foreign/sys/login": var body map[string]string json.NewDecoder(r.Body).Decode(&body)