diff --git a/server/app/goauto/yeekeclient/client.go b/server/app/goauto/yeekeclient/client.go index 57f6f03..ab9e64c 100644 --- a/server/app/goauto/yeekeclient/client.go +++ b/server/app/goauto/yeekeclient/client.go @@ -110,10 +110,10 @@ func (c *Client) do(ctx context.Context, method, path string, body any, query ur if body != nil { req.Header.Set("Content-Type", "application/json") } + // The yeeke web client authenticates with the X-Access-Token header (HAR); + // keeping the token out of the URL also keeps it out of any access log. if c.token != "" { - q := req.URL.Query() - q.Set("token", c.token) - req.URL.RawQuery = q.Encode() + req.Header.Set("X-Access-Token", c.token) } resp, e := c.http.Do(req) if e != nil { @@ -165,7 +165,7 @@ func (e *APIError) Is(target error) bool { if target != ErrSessionInvalid { return false } - return e.Code == 401 || strings.Contains(e.Message, "token") || strings.Contains(e.Message, "登录已过期") || strings.Contains(e.Message, "未登录") || strings.Contains(e.Message, "重新登录") + return e.Code == 401 || strings.Contains(e.Message, "token") || strings.Contains(e.Message, "登录已过期") || strings.Contains(e.Message, "未登录") || strings.Contains(e.Message, "重新登录") || (strings.Contains(e.Message, "登录") && strings.Contains(e.Message, "失效")) } // isCaptchaRejected reports a wrong captcha, the only login failure worth @@ -291,7 +291,10 @@ func (c *Client) Login(ctx context.Context, username, password, captcha, checkKe return &LoginResult{Token: p.Token, UserID: fmt.Sprint(p.UserInfo.ID), Username: p.UserInfo.Username, ExpiresAt: time.Now().UTC().Add(24 * time.Hour)}, nil } func (c *Client) CheckSession(ctx context.Context) error { - _, e := c.do(ctx, http.MethodGet, "/agent-foreign/sys/userInfo", nil, nil) + // /shopee/user/info is what the web client itself calls after login (HAR); + // the earlier /sys/userInfo path does not exist and answered HTTP 500. + q := url.Values{"_t": {strconv.FormatInt(time.Now().UnixMilli(), 10)}} + _, e := c.do(ctx, http.MethodGet, "/agent-foreign/shopee/user/info", nil, q) return e } @@ -343,7 +346,8 @@ type ReturnItem struct { } func (c *Client) List(ctx context.Context, pageNo, pageSize int) (ReturnPage, error) { - body := map[string]any{"pageNo": pageNo, "pageSize": pageSize, "claimFlag": 1, "status": 1, "relationFlag": 1, "orderBy": "createTime", "order": "desc"} + // Same shape the web client posts (HAR): sort via column/order, filters as strings. + body := map[string]any{"pageNo": pageNo, "pageSize": pageSize, "claimFlag": "1", "status": "1", "relationFlag": "1", "column": "createTime", "order": "desc"} raw, e := c.do(ctx, http.MethodPost, "/agent-foreign/packageClaimRec/relation/list", body, nil) if e != nil { return ReturnPage{}, e diff --git a/server/app/goauto/yeekeclient/client_test.go b/server/app/goauto/yeekeclient/client_test.go index e6966fe..5fbe066 100644 --- a/server/app/goauto/yeekeclient/client_test.go +++ b/server/app/goauto/yeekeclient/client_test.go @@ -19,8 +19,16 @@ func TestCaptchaLoginAndReadOnlyList(t *testing.T) { 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": - if r.URL.Query().Get("token") != "opaque" { - t.Errorf("token missing") + if r.Header.Get("X-Access-Token") != "opaque" { + t.Errorf("X-Access-Token header missing") + } + if r.URL.Query().Get("token") != "" { + t.Errorf("token must not be sent in the URL") + } + var body map[string]any + _ = json.NewDecoder(r.Body).Decode(&body) + if body["column"] != "createTime" || body["claimFlag"] != "1" || body["status"] != "1" || body["relationFlag"] != "1" || body["order"] != "desc" { + t.Errorf("list body shape differs from the web client: %v", body) } json.NewEncoder(w).Encode(map[string]any{"success": true, "result": map[string]any{"records": []any{}, "total": 0}}) default: @@ -201,7 +209,7 @@ func TestLoginWithOCRRetriesOnlyCaptchaRejections(t *testing.T) { } func TestAPIErrorMapsExpiredLoginToSessionInvalid(t *testing.T) { - for msg, want := range map[string]bool{"token失效,请重新登录": true, "未登录": true, "验证码错误": false, "用户名或密码错误": false} { + for msg, want := range map[string]bool{"token失效,请重新登录": true, "未登录": true, "登录已失效": true, "验证码错误": false, "用户名或密码错误": false} { if got := errors.Is(&APIError{Code: 500, Message: msg}, ErrSessionInvalid); got != want { t.Fatalf("%q: errors.Is(ErrSessionInvalid) = %v, want %v", msg, got, want) } diff --git a/server/app/goauto/yeekeclient/connect_test.go b/server/app/goauto/yeekeclient/connect_test.go index 82f18d5..7e99a5f 100644 --- a/server/app/goauto/yeekeclient/connect_test.go +++ b/server/app/goauto/yeekeclient/connect_test.go @@ -49,15 +49,16 @@ func fakeServer(t *testing.T, loginOK func(captcha string) bool, sessionValid fu case "/agent-foreign/sys/randomImage": 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 + var body map[string]any json.NewDecoder(r.Body).Decode(&body) - if loginOK(body["captcha"]) { - json.NewEncoder(w).Encode(map[string]any{"success": true, "result": map[string]any{"token": "tok-" + body["captcha"], "userInfo": map[string]any{"id": "u1", "username": "u"}}}) + captcha, _ := body["captcha"].(string) + if loginOK(captcha) { + json.NewEncoder(w).Encode(map[string]any{"success": true, "result": map[string]any{"token": "tok-" + captcha, "userInfo": map[string]any{"id": "u1", "username": "u"}}}) return } json.NewEncoder(w).Encode(map[string]any{"success": false, "code": 1, "message": "验证码错误"}) - case "/agent-foreign/sys/userInfo": - token := r.URL.Query().Get("token") + case "/agent-foreign/shopee/user/info": + token := r.Header.Get("X-Access-Token") if sessionValid(token) { json.NewEncoder(w).Encode(map[string]any{"success": true, "result": map[string]any{}}) return