fix(yeeke): use real session-check endpoint, X-Access-Token header and web list body (#336)
Compared against the HAR: /agent-foreign/sys/userInfo does not exist and yeeke answered HTTP 500, so every sync after the first successful login failed at the session check. Use /agent-foreign/shopee/user/info, which the web client calls after login. Send the token in the X-Access-Token header like the web client (the list endpoint only accepts the header) instead of a ?token= URL parameter, which also keeps it out of URL logs. Post the list filters as the web client does (column/order, string flags). Also treat "登录...失效" as an expired session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user