From c15ae94307200c3ab4419b25c5312eae07abe5e1 Mon Sep 17 00:00:00 2001 From: foxxorcat <95907542+foxxorcat@users.noreply.github.com> Date: Sat, 13 Sep 2025 13:59:47 +0800 Subject: [PATCH] feat(189PC,189TV): add refreshToken and qrcode login (#1205) ### Key Changes - **189PC**: Add QR code login and refresh token support - **189TV**: Add session refresh mechanism and fix TempUuid persistence issue - **Both**: Implement session keep-alive with cron jobs (5min interval) ### Features - QR code authentication for 189PC as alternative to password login - Automatic token refresh to avoid frequent re-authentication - Session keep-alive to maintain long-term connections - Retry logic with max attempts to prevent infinite loops ### Fixes - Fixed 189TV TempUuid causing storage corruption on QR code reload - Enhanced error handling for token expiration scenarios --- drivers/189_tv/driver.go | 22 +++- drivers/189_tv/meta.go | 1 - drivers/189_tv/utils.go | 66 +++++++++- drivers/189pc/driver.go | 30 +++-- drivers/189pc/help.go | 14 ++ drivers/189pc/meta.go | 8 +- drivers/189pc/types.go | 31 +++-- drivers/189pc/utils.go | 276 ++++++++++++++++++++++++++++++++++----- 8 files changed, 382 insertions(+), 66 deletions(-) diff --git a/drivers/189_tv/driver.go b/drivers/189_tv/driver.go index cf943a42..d24ef529 100644 --- a/drivers/189_tv/driver.go +++ b/drivers/189_tv/driver.go @@ -1,7 +1,6 @@ package _189_tv import ( - "container/ring" "context" "net/http" "strconv" @@ -12,18 +11,20 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/pkg/cron" "github.com/go-resty/resty/v2" ) type Cloud189TV struct { model.Storage Addition - client *resty.Client - tokenInfo *AppSessionResp - uploadThread int - familyTransferFolder *ring.Ring - cleanFamilyTransferFile func() - storageConfig driver.Config + client *resty.Client + tokenInfo *AppSessionResp + uploadThread int + storageConfig driver.Config + + TempUuid string + cron *cron.Cron // 新增 cron 字段 } func (y *Cloud189TV) Config() driver.Config { @@ -79,10 +80,17 @@ func (y *Cloud189TV) Init(ctx context.Context) (err error) { } } + y.cron = cron.NewCron(time.Minute * 5) + y.cron.Do(y.keepAlive) + return } func (y *Cloud189TV) Drop(ctx context.Context) error { + if y.cron != nil { + y.cron.Stop() + y.cron = nil + } return nil } diff --git a/drivers/189_tv/meta.go b/drivers/189_tv/meta.go index efe344e3..f50fe7ea 100644 --- a/drivers/189_tv/meta.go +++ b/drivers/189_tv/meta.go @@ -8,7 +8,6 @@ import ( type Addition struct { driver.RootID AccessToken string `json:"access_token"` - TempUuid string OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"` OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"` Type string `json:"type" type:"select" options:"personal,family" default:"personal"` diff --git a/drivers/189_tv/utils.go b/drivers/189_tv/utils.go index 4692ee6b..395c5dcd 100644 --- a/drivers/189_tv/utils.go +++ b/drivers/189_tv/utils.go @@ -66,6 +66,10 @@ func (y *Cloud189TV) AppKeySignatureHeader(url, method string) map[string]string } func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, isFamily ...bool) ([]byte, error) { + return y.requestWithRetry(url, method, callback, params, resp, 0, isFamily...) +} + +func (y *Cloud189TV) requestWithRetry(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, retryCount int, isFamily ...bool) ([]byte, error) { req := y.client.R().SetQueryParams(clientSuffix()) if params != nil { @@ -91,7 +95,22 @@ func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, para if strings.Contains(res.String(), "userSessionBO is null") || strings.Contains(res.String(), "InvalidSessionKey") { - return nil, errors.New("session expired") + // 限制重试次数,避免无限递归 + if retryCount >= 3 { + y.Addition.AccessToken = "" + op.MustSaveDriverStorage(y) + return nil, errors.New("session expired after retry") + } + + // 尝试刷新会话 + if err := y.refreshSession(); err != nil { + // 如果刷新失败,说明AccessToken也已过期,需要重新登录 + y.Addition.AccessToken = "" + op.MustSaveDriverStorage(y) + return nil, errors.New("session expired") + } + // 如果刷新成功,则重试原始请求(增加重试计数) + return y.requestWithRetry(url, method, callback, params, resp, retryCount+1, isFamily...) } // 处理错误 @@ -211,7 +230,7 @@ func (y *Cloud189TV) login() (err error) { var erron RespErr var tokenInfo AppSessionResp if y.Addition.AccessToken == "" { - if y.Addition.TempUuid == "" { + if y.TempUuid == "" { // 获取登录参数 var uuidInfo UuidInfoResp req.SetResult(&uuidInfo).SetError(&erron) @@ -230,7 +249,7 @@ func (y *Cloud189TV) login() (err error) { if uuidInfo.Uuid == "" { return errors.New("uuidInfo is empty") } - y.Addition.TempUuid = uuidInfo.Uuid + y.TempUuid = uuidInfo.Uuid op.MustSaveDriverStorage(y) // 展示二维码 @@ -258,7 +277,7 @@ func (y *Cloud189TV) login() (err error) { // Signature req.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/qrcodeLoginResult.action", http.MethodGet)) - req.SetQueryParam("uuid", y.Addition.TempUuid) + req.SetQueryParam("uuid", y.TempUuid) _, err = req.Execute(http.MethodGet, ApiUrl+"/family/manage/qrcodeLoginResult.action") if err != nil { return @@ -270,7 +289,6 @@ func (y *Cloud189TV) login() (err error) { return errors.New("E189AccessToken is empty") } y.Addition.AccessToken = accessTokenResp.E189AccessToken - y.Addition.TempUuid = "" } } // 获取SessionKey 和 SessionSecret @@ -294,6 +312,44 @@ func (y *Cloud189TV) login() (err error) { return } +// refreshSession 尝试使用现有的 AccessToken 刷新会话 +func (y *Cloud189TV) refreshSession() (err error) { + var erron RespErr + var tokenInfo AppSessionResp + reqb := y.client.R().SetQueryParams(clientSuffix()) + reqb.SetResult(&tokenInfo).SetError(&erron) + // Signature + reqb.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/loginFamilyMerge.action", + http.MethodGet)) + reqb.SetQueryParam("e189AccessToken", y.Addition.AccessToken) + _, err = reqb.Execute(http.MethodGet, ApiUrl+"/family/manage/loginFamilyMerge.action") + if err != nil { + return + } + + if erron.HasError() { + return &erron + } + + y.tokenInfo = &tokenInfo + return nil +} + +func (y *Cloud189TV) keepAlive() { + _, err := y.get(ApiUrl+"/keepUserSession.action", func(r *resty.Request) { + r.SetQueryParams(clientSuffix()) + }, nil) + if err != nil { + utils.Log.Warnf("189tv: Failed to keep user session alive: %v", err) + // 如果keepAlive失败,尝试刷新session + if refreshErr := y.refreshSession(); refreshErr != nil { + utils.Log.Errorf("189tv: Failed to refresh session after keepAlive error: %v", refreshErr) + } + } else { + utils.Log.Debugf("189tv: User session kept alive successfully.") + } +} + func (y *Cloud189TV) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) { fileMd5 := stream.GetHash().GetHash(utils.MD5) if len(fileMd5) < utils.MD5.Width { diff --git a/drivers/189pc/driver.go b/drivers/189pc/driver.go index 49e3dd73..f7ba93c9 100644 --- a/drivers/189pc/driver.go +++ b/drivers/189pc/driver.go @@ -12,6 +12,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/pkg/cron" "github.com/OpenListTeam/OpenList/v4/pkg/utils" "github.com/go-resty/resty/v2" "github.com/google/uuid" @@ -21,12 +22,12 @@ type Cloud189PC struct { model.Storage Addition - identity string - client *resty.Client - loginParam *LoginParam - tokenInfo *AppSessionResp + loginParam *LoginParam + qrcodeParam *QRLoginParam + + tokenInfo *AppSessionResp uploadThread int @@ -35,6 +36,7 @@ type Cloud189PC struct { storageConfig driver.Config ref *Cloud189PC + cron *cron.Cron } func (y *Cloud189PC) Config() driver.Config { @@ -84,14 +86,22 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) { }) } - // 避免重复登陆 - identity := utils.GetMD5EncodeStr(y.Username + y.Password) - if !y.isLogin() || y.identity != identity { - y.identity = identity + // 先尝试用Token刷新,之后尝试登陆 + if y.Addition.RefreshToken != "" { + y.tokenInfo = &AppSessionResp{RefreshToken: y.Addition.RefreshToken} + if err = y.refreshToken(); err != nil { + return + } + } else { if err = y.login(); err != nil { return } } + + // 初始化并启动 cron 任务 + y.cron = cron.NewCron(time.Duration(time.Minute * 5)) + // 每5分钟执行一次 keepAlive + y.cron.Do(y.keepAlive) } // 处理家庭云ID @@ -128,6 +138,10 @@ func (d *Cloud189PC) InitReference(storage driver.Driver) error { func (y *Cloud189PC) Drop(ctx context.Context) error { y.ref = nil + if y.cron != nil { + y.cron.Stop() + y.cron = nil + } return nil } diff --git a/drivers/189pc/help.go b/drivers/189pc/help.go index 8bd90d47..6f6c59f3 100644 --- a/drivers/189pc/help.go +++ b/drivers/189pc/help.go @@ -80,6 +80,20 @@ func timestamp() int64 { return time.Now().UTC().UnixNano() / 1e6 } +// formatDate formats a time.Time object into the "YYYY-MM-DDHH:mm:ssSSS" format. +func formatDate(t time.Time) string { + // The layout string "2006-01-0215:04:05.000" corresponds to: + // 2006 -> Year (YYYY) + // 01 -> Month (MM) + // 02 -> Day (DD) + // 15 -> Hour (HH) + // 04 -> Minute (mm) + // 05 -> Second (ss) + // 000 -> Millisecond (SSS) with leading zeros + // Note the lack of a separator between the date and hour, matching the desired output. + return t.Format("2006-01-0215:04:05.000") +} + func MustParseTime(str string) *time.Time { lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05 -07", str+" +08", time.Local) return &lastOpTime diff --git a/drivers/189pc/meta.go b/drivers/189pc/meta.go index 22d39615..670b9911 100644 --- a/drivers/189pc/meta.go +++ b/drivers/189pc/meta.go @@ -6,9 +6,11 @@ import ( ) type Addition struct { - Username string `json:"username" required:"true"` - Password string `json:"password" required:"true"` - VCode string `json:"validate_code"` + LoginType string `json:"login_type" type:"select" options:"password,qrcode" default:"password" required:"true"` + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` + VCode string `json:"validate_code"` + RefreshToken string `json:"refresh_token" help:"To switch accounts, please clear this field"` driver.RootID OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"` OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"` diff --git a/drivers/189pc/types.go b/drivers/189pc/types.go index 6620483f..d629a2ad 100644 --- a/drivers/189pc/types.go +++ b/drivers/189pc/types.go @@ -68,15 +68,7 @@ func (e *RespErr) Error() string { return "" } -// 登陆需要的参数 -type LoginParam struct { - // 加密后的用户名和密码 - RsaUsername string - RsaPassword string - - // rsa密钥 - jRsaKey string - +type BaseLoginParam struct { // 请求头参数 Lt string ReqId string @@ -88,6 +80,27 @@ type LoginParam struct { CaptchaToken string } +// QRLoginParam 用于暂存二维码登录过程中的参数 +type QRLoginParam struct { + BaseLoginParam + + UUID string `json:"uuid"` + EncodeUUID string `json:"encodeuuid"` + EncryUUID string `json:"encryuuid"` +} + +// 登陆需要的参数 +type LoginParam struct { + // 加密后的用户名和密码 + RsaUsername string + RsaPassword string + + // rsa密钥 + jRsaKey string + + BaseLoginParam +} + // 登陆加密相关 type EncryptConfResp struct { Result int `json:"result"` diff --git a/drivers/189pc/utils.go b/drivers/189pc/utils.go index 12832ca8..fc95007d 100644 --- a/drivers/189pc/utils.go +++ b/drivers/189pc/utils.go @@ -29,6 +29,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/pkg/errgroup" "github.com/OpenListTeam/OpenList/v4/pkg/utils" + "github.com/skip2/go-qrcode" "github.com/avast/retry-go" "github.com/go-resty/resty/v2" @@ -54,6 +55,9 @@ const ( MAC = "TELEMAC" CHANNEL_ID = "web_cloud.189.cn" + + // Error codes + UserInvalidOpenTokenError = "UserInvalidOpenToken" ) func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string { @@ -264,7 +268,14 @@ func (y *Cloud189PC) findFileByName(ctx context.Context, searchName string, fold } } -func (y *Cloud189PC) login() (err error) { +func (y *Cloud189PC) login() error { + if y.LoginType == "qrcode" { + return y.loginByQRCode() + } + return y.loginByPassword() +} + +func (y *Cloud189PC) loginByPassword() (err error) { // 初始化登陆所需参数 if y.loginParam == nil { if err = y.initLoginParam(); err != nil { @@ -278,10 +289,15 @@ func (y *Cloud189PC) login() (err error) { // 销毁登陆参数 y.loginParam = nil // 遇到错误,重新加载登陆参数(刷新验证码) - if err != nil && y.NoUseOcr { - if err1 := y.initLoginParam(); err1 != nil { - err = fmt.Errorf("err1: %s \nerr2: %s", err, err1) + if err != nil { + if y.NoUseOcr { + if err1 := y.initLoginParam(); err1 != nil { + err = fmt.Errorf("err1: %s \nerr2: %s", err, err1) + } } + + y.Status = err.Error() + op.MustSaveDriverStorage(y) } }() @@ -336,14 +352,105 @@ func (y *Cloud189PC) login() (err error) { err = fmt.Errorf(tokenInfo.ResMessage) return } + y.Addition.RefreshToken = tokenInfo.RefreshToken y.tokenInfo = &tokenInfo + op.MustSaveDriverStorage(y) return } -/* 初始化登陆需要的参数 -* 如果遇到验证码返回错误 - */ -func (y *Cloud189PC) initLoginParam() error { +func (y *Cloud189PC) loginByQRCode() error { + if y.qrcodeParam == nil { + if err := y.initQRCodeParam(); err != nil { + // 二维码也通过错误返回 + return err + } + } + + var state struct { + Status int `json:"status"` + RedirectUrl string `json:"redirectUrl"` + Msg string `json:"msg"` + } + + now := time.Now() + _, err := y.client.R(). + SetHeaders(map[string]string{ + "Referer": AUTH_URL, + "Reqid": y.qrcodeParam.ReqId, + "lt": y.qrcodeParam.Lt, + }). + SetFormData(map[string]string{ + "appId": APP_ID, + "clientType": CLIENT_TYPE, + "returnUrl": RETURN_URL, + "paramId": y.qrcodeParam.ParamId, + "uuid": y.qrcodeParam.UUID, + "encryuuid": y.qrcodeParam.EncryUUID, + "date": formatDate(now), + "timeStamp": fmt.Sprint(now.UTC().UnixNano() / 1e6), + }). + ForceContentType("application/json;charset=UTF-8"). + SetResult(&state). + Post(AUTH_URL + "/api/logbox/oauth2/qrcodeLoginState.do") + if err != nil { + return fmt.Errorf("failed to check QR code state: %w", err) + } + + switch state.Status { + case 0: // 登录成功 + var tokenInfo AppSessionResp + _, err = y.client.R(). + SetResult(&tokenInfo). + SetQueryParams(clientSuffix()). + SetQueryParam("redirectURL", state.RedirectUrl). + Post(API_URL + "/getSessionForPC.action") + if err != nil { + return err + } + if tokenInfo.ResCode != 0 { + return fmt.Errorf(tokenInfo.ResMessage) + } + y.Addition.RefreshToken = tokenInfo.RefreshToken + y.tokenInfo = &tokenInfo + op.MustSaveDriverStorage(y) + return nil + case -11001: // 二维码过期 + y.qrcodeParam = nil + return errors.New("QR code expired, please try again") + case -106: // 等待扫描 + return y.genQRCode("QR code has not been scanned yet, please scan and save again") + case -11002: // 等待确认 + return y.genQRCode("QR code has been scanned, please confirm the login on your phone and save again") + default: // 其他错误 + y.qrcodeParam = nil + return fmt.Errorf("QR code login failed with status %d: %s", state.Status, state.Msg) + } +} + +func (y *Cloud189PC) genQRCode(text string) error { + // 展示二维码 + qrTemplate := ` + state: %s +
+
Or Click here: Login +` + + // Generate QR code + qrCode, err := qrcode.Encode(y.qrcodeParam.UUID, qrcode.Medium, 256) + if err != nil { + return fmt.Errorf("failed to generate QR code: %v", err) + } + + // Encode QR code to base64 + qrCodeBase64 := base64.StdEncoding.EncodeToString(qrCode) + + // Create the HTML page + qrPage := fmt.Sprintf(qrTemplate, text, qrCodeBase64, y.qrcodeParam.UUID) + return fmt.Errorf("need verify: \n%s", qrPage) + +} + +func (y *Cloud189PC) initBaseParams() (*BaseLoginParam, error) { // 清除cookie jar, _ := cookiejar.New(nil) y.client.SetCookieJar(jar) @@ -357,17 +464,30 @@ func (y *Cloud189PC) initLoginParam() error { }). Get(WEB_URL + "/api/portal/unifyLoginForPC.action") if err != nil { - return err + return nil, err } - param := LoginParam{ + return &BaseLoginParam{ CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1], Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1], ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1], ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1], - // jRsaKey: regexp.MustCompile(`"j_rsaKey" value="(.+?)"`).FindStringSubmatch(res.String())[1], + }, nil +} + +/* 初始化登陆需要的参数 + * 如果遇到验证码返回错误 + */ +func (y *Cloud189PC) initLoginParam() error { + y.loginParam = nil + + baseParam, err := y.initBaseParams() + if err != nil { + return err } + y.loginParam = &LoginParam{BaseLoginParam: *baseParam} + // 获取rsa公钥 var encryptConf EncryptConfResp _, err = y.client.R(). @@ -378,18 +498,17 @@ func (y *Cloud189PC) initLoginParam() error { return err } - param.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey) - param.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Username) - param.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Password) - y.loginParam = ¶m + y.loginParam.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey) + y.loginParam.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Username) + y.loginParam.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Password) // 判断是否需要验证码 resp, err := y.client.R(). - SetHeader("REQID", param.ReqId). + SetHeader("REQID", y.loginParam.ReqId). SetFormData(map[string]string{ "appKey": APP_ID, "accountType": ACCOUNT_TYPE, - "userName": param.RsaUsername, + "userName": y.loginParam.RsaUsername, }).Post(AUTH_URL + "/api/logbox/oauth2/needcaptcha.do") if err != nil { return err @@ -401,8 +520,8 @@ func (y *Cloud189PC) initLoginParam() error { // 拉取验证码 imgRes, err := y.client.R(). SetQueryParams(map[string]string{ - "token": param.CaptchaToken, - "REQID": param.ReqId, + "token": y.loginParam.CaptchaToken, + "REQID": y.loginParam.ReqId, "rnd": fmt.Sprint(timestamp()), }). Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do") @@ -429,10 +548,38 @@ func (y *Cloud189PC) initLoginParam() error { return nil } +// getQRCode 获取并返回二维码 +func (y *Cloud189PC) initQRCodeParam() (err error) { + y.qrcodeParam = nil + + baseParam, err := y.initBaseParams() + if err != nil { + return err + } + + var qrcodeParam QRLoginParam + _, err = y.client.R(). + SetFormData(map[string]string{"appId": APP_ID}). + ForceContentType("application/json;charset=UTF-8"). + SetResult(&qrcodeParam). + Post(AUTH_URL + "/api/logbox/oauth2/getUUID.do") + if err != nil { + return err + } + qrcodeParam.BaseLoginParam = *baseParam + y.qrcodeParam = &qrcodeParam + + return y.genQRCode("please scan the QR code with the 189 Cloud app, then save the settings again.") +} + // 刷新会话 func (y *Cloud189PC) refreshSession() (err error) { + return y.refreshSessionWithRetry(0) +} + +func (y *Cloud189PC) refreshSessionWithRetry(retryCount int) (err error) { if y.ref != nil { - return y.ref.refreshSession() + return y.ref.refreshSessionWithRetry(retryCount) } var erron RespErr var userSessionResp UserSessionResp @@ -449,24 +596,87 @@ func (y *Cloud189PC) refreshSession() (err error) { return err } - // 错误影响正常访问,下线该储存 - defer func() { - if err != nil { - y.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error())) - op.MustSaveDriverStorage(y) - } - }() - + // token生效刷新token if erron.HasError() { - if erron.ResCode == "UserInvalidOpenToken" { - if err = y.login(); err != nil { - return err - } + if erron.ResCode == UserInvalidOpenTokenError { + return y.refreshTokenWithRetry(retryCount) } return &erron } y.tokenInfo.UserSessionResp = userSessionResp - return + return nil +} + +// refreshToken 刷新token,失败时返回错误,不再直接调用login +func (y *Cloud189PC) refreshToken() (err error) { + return y.refreshTokenWithRetry(0) +} + +func (y *Cloud189PC) refreshTokenWithRetry(retryCount int) (err error) { + if y.ref != nil { + return y.ref.refreshTokenWithRetry(retryCount) + } + + // 限制重试次数,避免无限递归 + if retryCount >= 3 { + if y.Addition.RefreshToken != "" { + y.Addition.RefreshToken = "" + op.MustSaveDriverStorage(y) + } + return errors.New("refresh token failed after maximum retries") + } + + var erron RespErr + var tokenInfo AppSessionResp + _, err = y.client.R(). + SetResult(&tokenInfo). + ForceContentType("application/json;charset=UTF-8"). + SetError(&erron). + SetFormData(map[string]string{ + "clientId": APP_ID, + "refreshToken": y.tokenInfo.RefreshToken, + "grantType": "refresh_token", + "format": "json", + }). + Post(AUTH_URL + "/api/oauth2/refreshToken.do") + if err != nil { + return err + } + + // 如果刷新失败,返回错误给上层处理 + if erron.HasError() { + if y.Addition.RefreshToken != "" { + y.Addition.RefreshToken = "" + op.MustSaveDriverStorage(y) + } + + // 根据登录类型决定下一步行为 + if y.LoginType == "qrcode" { + return errors.New("QR code session has expired, please re-scan the code to log in") + } + // 密码登录模式下,尝试回退到完整登录 + return y.login() + } + + y.Addition.RefreshToken = tokenInfo.RefreshToken + y.tokenInfo = &tokenInfo + op.MustSaveDriverStorage(y) + return y.refreshSessionWithRetry(retryCount + 1) +} + +func (y *Cloud189PC) keepAlive() { + _, err := y.get(API_URL+"/keepUserSession.action", func(r *resty.Request) { + r.SetQueryParams(clientSuffix()) + }, nil) + if err != nil { + utils.Log.Warnf("189pc: Failed to keep user session alive: %v", err) + // 如果keepAlive失败,尝试刷新session + if refreshErr := y.refreshSession(); refreshErr != nil { + utils.Log.Errorf("189pc: Failed to refresh session after keepAlive error: %v", refreshErr) + } + } else { + utils.Log.Debugf("189pc: User session kept alive successfully.") + } } // 普通上传