diff --git a/internal/command/album_web.go b/internal/command/album_web.go index faf3799..81c4e15 100644 --- a/internal/command/album_web.go +++ b/internal/command/album_web.go @@ -24,6 +24,7 @@ import ( "github.com/tickstep/aliyunpan/internal/config" "github.com/tickstep/aliyunpan/internal/file/downloader" "github.com/tickstep/aliyunpan/internal/functions/pandownload" + "github.com/tickstep/aliyunpan/internal/functions/panlogin" "github.com/tickstep/aliyunpan/internal/taskframework" "github.com/tickstep/aliyunpan/internal/utils" "github.com/tickstep/aliyunpan/library/requester/transfer" @@ -628,7 +629,8 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) { go func(flag *int32) { for atomic.LoadInt32(flag) == 0 { time.Sleep(time.Duration(1) * time.Minute) - if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) { + re := RefreshWebTokenInNeed(activeUser) + if re.Code == panlogin.SUCCESS { logger.Verboseln("update access token for download task") } } diff --git a/internal/command/utils.go b/internal/command/utils.go index 6f8c531..579dbce 100644 --- a/internal/command/utils.go +++ b/internal/command/utils.go @@ -23,6 +23,7 @@ import ( "github.com/tickstep/aliyunpan/internal/utils" "github.com/tickstep/library-go/logger" "github.com/urfave/cli" + "math" "math/rand" "net/url" "path" @@ -142,9 +143,9 @@ func NewWebLoginToken(accessToken string, expired int64) aliyunpan_web.WebLoginT } // RefreshWebTokenInNeed 刷新 webapi access token -func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool { +func RefreshWebTokenInNeed(activeUser *config.PanUser) *panlogin.CommonResultError { if activeUser == nil { - return false + return panlogin.NewCommonResultError("activeUser is nil") } // refresh expired web token @@ -153,7 +154,7 @@ func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool { cz := time.FixedZone("CST", 8*3600) // 东8区 expiredTime := time.Unix(activeUser.WebapiToken.Expired, 0).In(cz) now := time.Now() - if (expiredTime.Unix() - now.Unix()) <= (2 * 60) { // 有效期小于2min就刷新 + if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 有效期小于10min就刷新 pluginManger := plugins.NewPluginManager(config.GetPluginDir()) plugin, _ := pluginManger.GetPlugin() params := &plugins.UserTokenRefreshFinishParams{ @@ -167,9 +168,12 @@ func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool { // need update refresh token logger.Verboseln("web access token expired, get new from server") loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost) - wt, e := loginHelper.GetWebapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().WebapiPanClient().GetAccessToken()) - if e != nil { - logger.Verboseln("get web token from server error: ", e) + wt, re := loginHelper.GetWebapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().WebapiPanClient().GetAccessToken()) + if re != nil { + logger.Verboseln("get web token from server error: ", re.Msg) + if re.Code != panlogin.SUCCESS { + return re + } } if wt != nil { params.Result = "success" @@ -191,42 +195,51 @@ func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool { } // create new signature - _, e := activeUser.PanClient().WebapiPanClient().CreateSession(nil) - if e != nil { - logger.Verboseln("call CreateSession error in RefreshWebTokenInNeed: " + e.Error()) + _, e1 := activeUser.PanClient().WebapiPanClient().CreateSession(nil) + if e1 != nil { + logger.Verboseln("call CreateSession error in RefreshWebTokenInNeed: " + e1.Error()) + } + return &panlogin.CommonResultError{ + Code: 0, + Msg: "", } - return true } else { // token refresh error // if token has expired, callback plugin api for notify if now.Unix() >= expiredTime.Unix() { params.Result = "fail" - params.Message = e.Error() + params.Message = re.Msg params.OldToken = activeUser.WebapiToken.AccessToken if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil { logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error()) } } } + } else { + return &panlogin.CommonResultError{ + Code: 0, + Msg: "web token is validate, not need to refresh", + } } } } - return false + return panlogin.NewCommonResultError("activeUser is invalidate") } // RefreshOpenTokenInNeed 刷新 openapi access token -func RefreshOpenTokenInNeed(activeUser *config.PanUser) bool { +func RefreshOpenTokenInNeed(activeUser *config.PanUser) *panlogin.CommonResultError { if activeUser == nil { - return false + return panlogin.NewCommonResultError("activeUser is nil") } + // OpenAPI的token只有2个小时有效期,需要在token过期之前进行刷新并把获取到的新token存储在本地 // refresh expired openapi token if activeUser.PanClient().OpenapiPanClient() != nil { if activeUser.OpenapiToken != nil && len(activeUser.OpenapiToken.AccessToken) > 0 { cz := time.FixedZone("CST", 8*3600) // 东8区 expiredTime := time.Unix(activeUser.OpenapiToken.Expired, 0).In(cz) now := time.Now().In(cz) - if (expiredTime.Unix() - now.Unix()) <= (2 * 60) { // 有效期小于2min就刷新 + if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 有效期小于10min就刷新 pluginManger := plugins.NewPluginManager(config.GetPluginDir()) plugin, _ := pluginManger.GetPlugin() params := &plugins.UserTokenRefreshFinishParams{ @@ -240,11 +253,15 @@ func RefreshOpenTokenInNeed(activeUser *config.PanUser) bool { // need update refresh token logger.Verboseln("openapi access token expired, get new from server") loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost) - wt, e := loginHelper.GetOpenapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().OpenapiPanClient().GetAccessToken()) - if e != nil { - logger.Verboseln("get openapi token from server error: ", e) + wt, re := loginHelper.GetOpenapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().OpenapiPanClient().GetAccessToken()) + if re != nil { + logger.Verboseln("get openapi token from server error: ", re.Msg) + if re.Code != panlogin.SUCCESS { + return re + } } if wt != nil { + // 存储新token到本地 params.Result = "success" params.OldToken = activeUser.OpenapiToken.AccessToken params.NewToken = wt.AccessToken @@ -261,23 +278,109 @@ func RefreshOpenTokenInNeed(activeUser *config.PanUser) bool { logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error()) } - return true + return &panlogin.CommonResultError{ + Code: 0, + Msg: "", + } } else { // token refresh error // if token has expired, callback plugin api for notify if now.Unix() >= expiredTime.Unix() { params.Result = "fail" - params.Message = e.Error() - params.OldToken = activeUser.WebapiToken.AccessToken + params.Message = re.Msg + params.OldToken = activeUser.OpenapiToken.AccessToken if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil { logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error()) } } } + } else { + return &panlogin.CommonResultError{ + Code: 0, + Msg: "openapi token is validate, not need to refresh", + } } } } - return false + return panlogin.NewCommonResultError("activeUser is invalidate") +} + +// AutomaticallyRefreshTokenTask 自动刷新Token的后台任务 +func AutomaticallyRefreshTokenTask() { + go func() { + // 检测延迟时间(分钟) + delayMinuteTime := time.Duration(1) * time.Minute + //delayMinuteTime := time.Duration(10) * time.Second // for test + // 请求重试次数 + attempt := 1 + for { + // Token刷新进程,不管是CLI命令行模式,还是直接命令模式,本刷新任务都会执行 + time.Sleep(delayMinuteTime) // 定时器,每1分钟调用一次 + + // 重新加载配置文件 + ReloadConfigFunc(nil) + activeUser := config.Config.ActiveUser() + if activeUser != nil && activeUser.UserId != "" && activeUser.PanClient() != nil { + // 刷新Openapi端Token + errResult := RefreshOpenTokenInNeed(activeUser) + attempt += 1 + if errResult != nil && errResult.Code == panlogin.SUCCESS { + // 刷新成功,重置定时器 + attempt = 1 + delayMinuteTime = time.Duration(1) * time.Minute + + // 刷新Web端Token + RefreshWebTokenInNeed(activeUser) + + // 保存新token到配置文件 + SaveConfigFunc(nil) + } else { + // 刷新Token失败 + logger.Verboseln("刷新Token失败:", errResult.Msg) + if errResult.Code == panlogin.ERROR_NEED_LOGIN_AGAIN { + // token已经失效,需要重新登录 + logger.Verboseln("token已经失效,需要重新登录,退出自动刷新Token任务") + return + } else if errResult.Code == panlogin.ERROR { + // 访问错误 + delayMinuteTime = ProgressiveBackoffAlg(attempt) + } else if errResult.Code == panlogin.ERROR_TOO_MANY_REQUESTS { + // 请求过于频繁 + delayMinuteTime = ProgressiveBackoffAlg(attempt) + } + } + } + } + }() +} + +// ProgressiveBackoffAlg 指数退降算法 +func ProgressiveBackoffAlg(attempt int) time.Duration { + maxRetries := 120 + baseDelay := float64(1) + maxDelay := float64(1440) + + if attempt >= maxRetries { + return time.Duration(maxDelay) * time.Minute + } + + // 计算当前延迟 + var delay float64 + if attempt < 100 { + // 前 100 次:线性增长 + 小指数基数(缓慢增加) + delay = baseDelay * (1 + float64(attempt)/5) // 调整分母控制增长速度 + } else { + // 100 次后:标准指数退避 + delay = baseDelay * math.Pow(1.5, float64(attempt-100)) + if delay > maxDelay { + delay = maxDelay + } + } + + // 添加随机抖动(最多 20% 的扰动) + jitterSec := rand.Float64() * delay * 0.2 // ±20% 随机扰动 + delay += jitterSec + return time.Duration(delay * float64(time.Minute)) } func isIncludeFile(pattern string, fileName string) bool { diff --git a/internal/command/utils_test.go b/internal/command/utils_test.go new file mode 100644 index 0000000..69c0fb4 --- /dev/null +++ b/internal/command/utils_test.go @@ -0,0 +1,13 @@ +package command + +import ( + "fmt" + "testing" +) + +func TestProgressiveBackoffAlg(t *testing.T) { + for attempt := 1; attempt <= 150; attempt++ { + delayTime := ProgressiveBackoffAlg(attempt) + fmt.Printf("第%d次,延迟分钟: %.2f\n", attempt, delayTime.Minutes()) + } +} diff --git a/internal/config/pan_user.go b/internal/config/pan_user.go index 4abb137..dbfe744 100644 --- a/internal/config/pan_user.go +++ b/internal/config/pan_user.go @@ -161,7 +161,7 @@ doOpenLoginAct: tryRefreshOpenToken = false wt, e := loginHelper.GetOpenapiNewToken(ticketId, userId, openapiToken.AccessToken) if e != nil { - logger.Verboseln("get openapi token from server error: ", e) + logger.Verboseln("get openapi token from server error: ", e.Msg) return nil, apierror.NewFailedApiError("get new openapi token error, try login again") } if wt != nil { @@ -169,7 +169,11 @@ doOpenLoginAct: AccessToken: wt.AccessToken, Expired: wt.Expired, } + + // 检查token有效期 } + + // 回到token检测流程,必须确保token是有效 time.Sleep(time.Duration(1) * time.Second) goto doOpenLoginAct } @@ -207,7 +211,7 @@ doWebLoginAct: tryRefreshWebToken = false wt, e := loginHelper.GetWebapiNewToken(ticketId, userId, webapiToken.AccessToken) if e != nil { - logger.Verboseln("get web token from server error: ", e) + logger.Verboseln("get web token from server error: ", e.Msg) } if wt != nil { webapiToken = &PanClientToken{ diff --git a/internal/functions/panlogin/login_helper.go b/internal/functions/panlogin/login_helper.go index 5013391..557d54c 100644 --- a/internal/functions/panlogin/login_helper.go +++ b/internal/functions/panlogin/login_helper.go @@ -21,8 +21,19 @@ type LoginHelper struct { webHost string } -// 通用接口错误 -type respCommonResult struct { +const ( + // SUCCESS 成功 + SUCCESS = 0 + // ERROR 失败 + ERROR = 500 + // ERROR_TOO_MANY_REQUESTS 请求太频繁,已被限流 + ERROR_TOO_MANY_REQUESTS = 429 + // ERROR_NEED_LOGIN_AGAIN Token已经过期,需要重新登录 + ERROR_NEED_LOGIN_AGAIN = 100004 +) + +// CommonResultError 通用接口错误 +type CommonResultError struct { Code int `json:"code"` Msg string `json:"msg"` } @@ -57,6 +68,13 @@ type CommonTokenEntity struct { Webapi *LoginTokenResult `json:"webapi"` } +func NewCommonResultError(msg string) *CommonResultError { + return &CommonResultError{ + Code: 500, + Msg: msg, + } +} + func NewLoginHelper(webHost string) *LoginHelper { return &LoginHelper{ webHost: webHost, @@ -93,7 +111,7 @@ func (h *LoginHelper) GetQRCodeLoginUrl(keyStr string) (*QRCodeUrlResult, error) } // 通用接口错误 - commonResult := &respCommonResult{} + commonResult := &CommonResultError{} if err1 := json.Unmarshal(body, commonResult); err1 != nil { logger.Verboseln("parse openapi result json error ", err1) return nil, err1 @@ -154,7 +172,7 @@ func (h *LoginHelper) GetQRCodeLoginResult(tokenId string) (*QRCodeLoginResult, } // 通用接口错误 - commonResult := &respCommonResult{} + commonResult := &CommonResultError{} if err1 := json.Unmarshal(body, commonResult); err1 != nil { logger.Verboseln("parse openapi result json error ", err1) return nil, err1 @@ -204,7 +222,7 @@ func (h *LoginHelper) GetRefreshToken(tokenId string) (*QRCodeLoginResult, error } // 通用接口错误 - commonResult := &respCommonResult{} + commonResult := &CommonResultError{} if err1 := json.Unmarshal(body, commonResult); err1 != nil { logger.Verboseln("parse openapi result json error ", err1) return nil, err1 @@ -269,7 +287,7 @@ func (h *LoginHelper) ParseSecureRefreshToken(keyStr, secureRefreshToken string) } // GetWebapiNewToken 获取Webapi Token -func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, error) { +func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, *CommonResultError) { fullUrl := strings.Builder{} fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/webapi/%s/refresh?userId=%s", h.webHost, ticketId, userId) @@ -287,26 +305,29 @@ func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string) body, err := client.Fetch("GET", fullUrl.String(), nil, header) if err != nil { logger.Verboseln("get web token result error ", err) - return nil, err + return nil, NewCommonResultError(err.Error()) } // 通用接口错误 - commonResult := &respCommonResult{} + commonResult := &CommonResultError{} if err1 := json.Unmarshal(body, commonResult); err1 != nil { logger.Verboseln("parse openapi result json error ", err1) - return nil, err1 + return nil, NewCommonResultError(err1.Error()) } if commonResult.Code != 0 { - return nil, fmt.Errorf(commonResult.Msg) + return nil, commonResult } errResp := &LoginHttpResult{} if err1 := json.Unmarshal(body, errResp); err1 != nil { logger.Verboseln("parse result json error ", err1) - return nil, err1 + return nil, NewCommonResultError(err1.Error()) } if errResp.Code != 0 { - return nil, fmt.Errorf(errResp.Msg) + return nil, &CommonResultError{ + Code: errResp.Code, + Msg: errResp.Msg, + } } // parse result @@ -314,13 +335,13 @@ func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string) r.Data = &LoginTokenResult{} if err2 := json.Unmarshal(body, r); err2 != nil { logger.Verboseln("parse web token result json error ", err2) - return nil, err2 + return nil, NewCommonResultError(err2.Error()) } return r.Data.(*LoginTokenResult), nil } // GetOpenapiNewToken 获取Openapi Token -func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, error) { +func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, *CommonResultError) { fullUrl := strings.Builder{} fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/openapi/%s/refresh?userId=%s", h.webHost, ticketId, userId) @@ -338,27 +359,30 @@ func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string body, err := client.Fetch("GET", fullUrl.String(), nil, header) if err != nil { logger.Verboseln("get openapi token result error ", err) - return nil, err + return nil, NewCommonResultError(err.Error()) } // 通用接口错误 - commonResult := &respCommonResult{} + commonResult := &CommonResultError{} if err1 := json.Unmarshal(body, commonResult); err1 != nil { logger.Verboseln("parse openapi result json error ", err1) - return nil, err1 + return nil, NewCommonResultError(err1.Error()) } if commonResult.Code != 0 { - return nil, fmt.Errorf(commonResult.Msg) + return nil, commonResult } // 解析结果 errResp := &LoginHttpResult{} if err1 := json.Unmarshal(body, errResp); err1 != nil { logger.Verboseln("parse result json error ", err1) - return nil, err1 + return nil, NewCommonResultError(err1.Error()) } if errResp.Code != 0 { - return nil, fmt.Errorf(errResp.Msg) + return nil, &CommonResultError{ + Code: errResp.Code, + Msg: errResp.Msg, + } } // parse result @@ -366,7 +390,7 @@ func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string r.Data = &LoginTokenResult{} if err2 := json.Unmarshal(body, r); err2 != nil { logger.Verboseln("parse openapi token result json error ", err2) - return nil, err2 + return nil, NewCommonResultError(err2.Error()) } return r.Data.(*LoginTokenResult), nil } @@ -393,7 +417,7 @@ func (h *LoginHelper) GetLoginToken(ticketId string) (*CommonTokenEntity, error) } // 通用接口错误 - commonResult := &respCommonResult{} + commonResult := &CommonResultError{} if err1 := json.Unmarshal(body, commonResult); err1 != nil { logger.Verboseln("parse openapi result json error ", err1) return nil, err1