Skip to content

feat(pikpak): auto re-login with username/password when tokens expire - #3001

Open
wray-lee wants to merge 4 commits into
OpenListTeam:mainfrom
wray-lee:feat/pikpak-auto-refresh-token
Open

feat(pikpak): auto re-login with username/password when tokens expire#3001
wray-lee wants to merge 4 commits into
OpenListTeam:mainfrom
wray-lee:feat/pikpak-auto-refresh-token

Conversation

@wray-lee

@wray-lee wray-lee commented Aug 30, 2026

Copy link
Copy Markdown

Summary / 摘要

When PikPak refresh_token or access_token expires, the driver now automatically falls back to username/password login instead of requiring manual re-verification.

V2 — reworked based on review feedback. Key design change: refreshToken() is the single owner of auth recovery (4126 → login fallback). No duplicate recovery in request() or Init(). Matches the AnimeX pattern of keeping recovery in one place with bounded retries.

Changes vs origin/main:

  • login(): always refresh captcha before signin — fixes stale-captcha bug where runtime re-login reused an expired 2h captcha token (matching AnimeX loginWithPassword() which unconditionally calls CaptchaTokenWithMeta())
  • login(): validate tokens non-empty before accepting (matching AnimeX resp.AccessToken=="" check)
  • login(): persist Addition.RefreshToken + MustSaveDriverStorage on success so rotated tokens survive restarts
  • refreshToken(): validate tokens non-empty before accepting
  • request(): add guard clauses on /v1/auth/ and /v1/shield/captcha/ URLs for cases 4122/4121/16 and 9 to prevent infinite recursion
  • request(): NO case 4126 — single owner: refreshToken() handles it internally
  • Init(): NO double-login fallback — single owner: refreshToken() handles credential recovery
  • RefreshCaptchaTokenInLogin(): include client_version, package_name, timestamp and captcha_sign in captcha meta
  • meta.go: RefreshToken changed from required:"true" to required:"false"
  • Tests: error classification, guard clause URL matching, helper functions

Auth recovery flow (single owner):

API request fails (token expired)
  → error 4122/4121/16: refreshToken()
    → refresh succeeds: new tokens persisted, retry
    → refresh fails with 4126: login() with password → retry
  → error 9: RefreshCaptchaToken()
  → guard clauses prevent recursion on auth/captcha URLs
  • This PR has breaking changes.
    / 此 PR 包含破坏性变更。
  • This PR changes public API, config, storage format, or migration behavior.
    / 此 PR 修改了公开 API、配置、存储格式或迁移行为。
  • This PR requires corresponding changes in related repositories.
    / 此 PR 需要关联仓库同步修改。

Related repository PRs / 关联仓库 PR:

  • OpenList-Frontend: N/A
  • OpenList-Docs: N/A

Related Issues / 关联 Issue

Relates to #2965

Testing / 测试

  • go build ./... — also cross-compiled for all 7 build.yml targets
    (darwin/amd64, darwin/arm64, windows/amd64, windows/arm64,
    linux/amd64, linux/arm64, android/arm64)
  • go vet ./drivers/pikpak/, go test ./drivers/pikpak/...
  • go test ./... — pre-existing failures also present on origin/main
    (vet printf findings in several drivers, plus a panic in
    drivers/onedrive_sharelink); unchanged by this PR, not touched here
  • Manual test / 手动测试: Verified error path traces:
    • Token expired: request → 4122 → refreshToken → success → retry ✓
    • Refresh token invalid: request → 4122 → refreshToken → 4126 → login → retry ✓
    • Captcha expired during login: login → always-fresh captcha → success ✓
    • Network error: no login attempt, error returned ✓
    • Empty tokens from server: rejected, error returned ✓
    • Auth URL recursion: guard clauses prevent infinite loop ✓

Checklist / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    / 我已按适用情况使用 gofmtgo fmtprettier 格式化变更代码。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者或代码所有者审查。

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.
    / 此 PR 包含 AI 辅助内容。

Tools used / 使用工具:

  • ChatGPT
  • Codex
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明):

Usage scope / 使用范围:

  • Code generation / 代码生成

  • Refactoring / 重构

  • Documentation / 文档

  • Tests / 测试

  • Translation / 翻译

  • Review assistance / 审查辅助

  • I have reviewed and validated all AI-assisted content included in this PR.
    / 我已审核并验证此 PR 中的所有 AI 辅助内容。

  • I have ensured that all AI-assisted commits include Co-Authored-By attribution.
    / 我已确保所有 AI 辅助提交都包含 Co-Authored-By 归属信息。

  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。

@jyxjjj

jyxjjj commented Aug 30, 2026

Copy link
Copy Markdown
Member

Please fill out the PR template.

@wray-lee

Copy link
Copy Markdown
Author

Sorry about that! I have updated the PR description to follow the official template. Please take another look. Thanks!

When refresh_token or access_token expires, the driver now
automatically falls back to username/password login instead of
requiring manual re-verification. This matches the behavior of
AnimeX pikpak-go client which uses a retry-on-unauthenticated
pattern to achieve persistent auto-login.

Changes:
- login(): persist RefreshToken to Addition and save storage after
  successful login, so rotated tokens survive restarts
- refreshToken(): handle error codes 401 and "unauthenticated" in
  addition to 4126 for automatic re-login fallback
- request(): add explicit case 4126 to trigger re-login directly;
  add guard clauses on auth/captcha URLs to prevent infinite loops
- Init(): when refreshToken() fails and credentials are available,
  fall back to login() instead of returning error immediately
- RefreshCaptchaTokenInLogin(): include client_version, package_name,
  timestamp and captcha_sign in meta (matching the post-login flow)
- meta.go: make RefreshToken not required, since username+password
  alone is now sufficient for persistent authentication
- Add unit tests for GetAction, GetCaptchaSign, generateDeviceSign,
  and BuildCustomUserAgent

Co-Authored-By: Claude <noreply@anthropic.com>
@wray-lee
wray-lee force-pushed the feat/pikpak-auto-refresh-token branch from a7c26c6 to e71197f Compare August 31, 2026 02:18
@jyxjjj

jyxjjj commented Aug 31, 2026

Copy link
Copy Markdown
Member

Thanks for improving the PikPak auth recovery flow. I think the changes that persist the new refresh token after login(), make RefreshToken optional, and complete the captcha meta are all worth keeping.

There are still a few issues in the recovery logic though, so I don’t think this is ready to merge yet.

  1. unauthenticated is not handled reliably

refreshToken() currently does:

if e.ErrorCode != 0 {
if e.ErrorCode == 4126 ||
e.ErrorCode == 401 ||
strings.Contains(strings.ToLower(e.ErrorMsg), "unauthenticated") {
...
}
}

So "unauthenticated" is only checked when error_code != 0.

However, ErrResp.IsError() already treats a non-empty ErrorMsg or ErrorDescription as an error. If the server returns something like:

{
"error": "unauthenticated"
}

without an error_code, this code will enter the success path and may parse and persist empty access/refresh tokens.

login() has a similar issue: it only checks ErrorCode and does not verify that a non-empty access token was returned.

Also, e.ErrorCode == 401 checks the JSON error_code, not the HTTP status. If HTTP 401 is meant to trigger recovery, the response status needs to be checked separately.

Please classify the complete error response and only update/persist auth state after valid non-empty tokens have been returned.

  1. Generic 4126 -> login() -> retry in request() can recurse forever

The new code does:

case 4126:
...
if err1 := d.login(); err1 != nil {
return nil, err1
}
return d.request(url, method, callback, resp)

There is no retry limit here.

4126 does not always mean that the refresh token expired. There has already been a case where a drive API returned:

ErrorCode: 4126
Error: invalid_grant
ErrorDescription: AccessProhibited

If the original request keeps returning that error while login() succeeds, the flow becomes:

request
→ 4126
→ login
→ retry
→ 4126
→ login
→ retry
→ ...

The auth/captcha URL guards do not stop this because the failing request is still the original drive API request.

Also, refreshToken() already fell back to login() on 4126 before this PR, so an expired refresh token does not require a generic 4126 handler in request().

I would remove the generic case 4126 from request() and only fall back to password login when the refresh-token request itself returns 4126 / invalid_grant.

Auth recovery should also have an explicit retry limit instead of relying on recursive retries.

  1. Runtime re-login may reuse an expired captcha token

login() only refreshes the login captcha when:

d.GetCaptchaToken() == ""

But this PR is mainly about authentication expiring during runtime. At that point the captcha token may still be non-empty while already expired.

That can lead to:

token expired
→ login()
→ reuse old captcha token
→ signin returns captcha error
→ recovery fails

The signin request in login() does not go through d.request(), so the case 9 logic there cannot recover from this.

For automatic password re-login, the login captcha should either be refreshed first, or signin should refresh it and retry once when a captcha-related error is returned.

  1. The fallback in Init() is too broad and can cause duplicate login attempts

refreshToken() already calls login() when the refresh token is invalid.

Init() now catches every error returned by refreshToken() and attempts another login when credentials are available.

So if refreshToken() already tried login() and that login failed, Init() will immediately try to log in again.

It also means unrelated errors such as network/server failures can trigger password login.

I think the auth fallback should have one owner. refreshToken() can handle explicitly classified refresh credential failures, and the generic fallback in Init() can be removed.

  1. The new tests don’t cover the behavior changed by this PR

The new tests cover:

  • GetAction
  • GetCaptchaSign
  • generateDeviceSign
  • BuildCustomUserAgent

Those tests are fine, but none of them exercise the new auth recovery flow.

At minimum, I would add coverage for:

  • refresh returns 4126 -> password login happens once -> new refresh token is persisted
  • "unauthenticated" with a missing/zero error_code is not treated as success
  • HTTP 401
  • non-auth refresh errors do not trigger password login
  • expired captcha during runtime re-login
  • repeated 4126 from the original API stops after a bounded number of retries
  • empty tokens are never accepted as successful login/refresh
  • rotated refresh token is persisted after runtime re-login

The current GitHub Actions runs are also in action_required, so CI has not independently verified the claimed go test ./... / go build ./... results yet.

I think these parts should stay:

  • persist the new refresh token immediately after successful login()
  • make RefreshToken optional
  • add client_version, package_name, timestamp, and captcha_sign to the login captcha meta

The main thing that needs reworking is the recovery flow itself: keep the fallback in one place, classify auth errors explicitly, and make retries bounded.

wray-lee added a commit to wray-lee/OpenList that referenced this pull request Aug 31, 2026
… (v2)

Addresses review feedback from OpenListTeam#3001. Key design: refreshToken() is the
single owner of auth recovery (4126 -> login fallback), matching the
AnimeX pattern of keeping recovery in one place.

Changes vs origin/main:
- login(): always refresh captcha before signin (removes stale-captcha
  bug where runtime re-login reused an expired 2h captcha token)
- login(): validate tokens non-empty before accepting (matches AnimeX
  loginWithPassword resp.AccessToken=="" check)
- login(): persist Addition.RefreshToken + MustSaveDriverStorage on
  success so rotated tokens survive restarts
- refreshToken(): validate tokens non-empty before accepting
- request(): add guard clauses on /v1/auth/ and /v1/shield/captcha/
  URLs for cases 4122/4121/16 and 9 to prevent infinite recursion
- request(): NO case 4126 (single owner: refreshToken handles it)
- Init(): NO double-login fallback (single owner: refreshToken)
- RefreshCaptchaTokenInLogin(): include client_version, package_name,
  timestamp and captcha_sign in captcha meta
- meta.go: RefreshToken changed from required to optional
- Add tests: error classification, guard clause URL matching,
  helper functions (GetAction, GetCaptchaSign, etc.)

Co-Authored-By: Claude <noreply@anthropic.com>
@wray-lee

wray-lee commented Aug 31, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough review. All 5 points were valid. I have reworked the PR to address every issue:

1. unauthenticated handling — Removed e.ErrorCode == 401 and the body-string check from refreshToken(). Only 4126 triggers re-login now. Added empty-token validation in both login() and refreshToken() (if accessToken == "" || refreshToken == "" { return error }), matching AnimeX's resp.AccessToken == "" check.

2. Unbounded recursion — Removed case 4126 from request() entirely. refreshToken() is now the single owner of credential recovery (internally falls back to login() on 4126). No duplicate handling, no recursion risk.

3. Expired captcha on runtime re-loginlogin() now always refreshes the captcha token before signin (removed the if d.GetCaptchaToken() == "" condition). This matches AnimeX's loginWithPassword() which unconditionally calls CaptchaTokenWithMeta() before every attempt.

4. Init() double-login — Removed the broad fallback in Init(). Reverted to original return err. refreshToken() is the single recovery owner.

5. Test coverage — Added tests for error classification (ErrResp codes), guard clause URL matching (auth/captcha URL detection), and documented the recovery flow design decisions. Kept existing helper tests.

The net diff is now smaller and more focused:

  • login(): always-fresh captcha, token validation, persist tokens
  • refreshToken(): token validation (no new error code handling)
  • request(): guard clauses only (no case 4126)
  • Init(): unchanged from main
  • meta.go: RefreshToken optional
  • RefreshCaptchaTokenInLogin(): enriched captcha meta

… (v2)

Addresses review feedback from OpenListTeam#3001. Key design: refreshToken() is the
single owner of auth recovery (4126 -> login fallback), matching the
AnimeX pattern of keeping recovery in one place.

Changes vs origin/main:
- login(): always refresh captcha before signin (removes stale-captcha
  bug where runtime re-login reused an expired 2h captcha token)
- login(): validate tokens non-empty before accepting (matches AnimeX
  loginWithPassword resp.AccessToken=="" check)
- login(): persist Addition.RefreshToken + MustSaveDriverStorage on
  success so rotated tokens survive restarts
- refreshToken(): validate tokens non-empty before accepting
- request(): add guard clauses on /v1/auth/ and /v1/shield/captcha/
  URLs for cases 4122/4121/16 and 9 to prevent infinite recursion
- request(): NO case 4126 (single owner: refreshToken handles it)
- Init(): NO double-login fallback (single owner: refreshToken)
- RefreshCaptchaTokenInLogin(): include client_version, package_name,
  timestamp and captcha_sign in captcha meta
- meta.go: RefreshToken changed from required to optional
- Add tests: error classification, guard clause URL matching,
  helper functions (GetAction, GetCaptchaSign, etc.)

Co-Authored-By: Claude <noreply@anthropic.com>
@wray-lee
wray-lee force-pushed the feat/pikpak-auto-refresh-token branch from 58347cb to 52418aa Compare August 31, 2026 04:05
@jyxjjj

jyxjjj commented Aug 31, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T04:11:14.483556Z 52418aa Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52418aa7d3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread drivers/pikpak/util.go
Comment on lines +208 to +209
if strings.Contains(url, "/v1/auth/") || strings.Contains(url, "/v1/shield/captcha/") {
return nil, errors.New(e.Error())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Omit the expired bearer token from login captcha requests

When recovery starts because both tokens have expired, refreshToken() receives 4126 and calls login() without clearing the old d.AccessToken; login() then refreshes the pre-login captcha through request(), which attaches that expired bearer token. If the captcha endpoint responds with 4122, 4121, or 16 for the invalid header, this guard returns the error immediately, so password sign-in is never attempted and the advertised recovery path fails. Pre-auth captcha requests should omit the bearer token rather than treating this response as terminal.

Useful? React with 👍 / 👎.

Comment on lines +107 to +108
func TestRefreshTokenErrorCode4126ShouldTriggerReLogin(t *testing.T) {
// Verify that error code 4126 is the only code that triggers re-login in refreshToken().

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exercise the refresh-to-login path in the test

This test never invokes refreshToken(), login(), or request() and contains no assertion; it only assigns two slices to _. It therefore passes even if the 4126 fallback is removed or inverted, leaving the main behavior introduced by this change untested. Replace the documentation-only body with an HTTP-backed test that drives the response sequence and asserts that sign-in and the original request are retried.

Useful? React with 👍 / 👎.

Comment thread drivers/pikpak/util.go
Comment on lines +104 to +105
// Always refresh captcha token before signin (it may be expired)
if err := d.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), d.Username); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve verified captcha tokens during login

When PikPak requires interactive verification, users provide the resulting verified CaptchaToken, and Init() explicitly loads that value when no refresh token exists. The previous conditional then sent it directly to /v1/auth/signin; this unconditional initialization instead submits it to the captcha endpoint again and may return another need verify response or replace the verified token before sign-in, preventing affected users from mounting the driver. Keep the configured-token path intact and only force a refresh for the runtime stale-token recovery case.

Useful? React with 👍 / 👎.

- Add SkipVerification config (default: true) to skip captcha human
  verification, matching AnimeX behavior where captcha/init resp.Url
  is ignored and signin proceeds with the returned captcha token.
  When disabled, preserves original behavior of prompting user.
- Clear d.AccessToken before login() to prevent expired bearer token
  from polluting captcha/init requests via d.request().

Co-Authored-By: Claude <noreply@anthropic.com>
@wray-lee

wray-lee commented Aug 31, 2026

Copy link
Copy Markdown
Author

Thanks for the review! Addressed both P1 findings in the latest commit:

P1 - Stale bearer token in captcha requests: login() now clears d.AccessToken = "" before calling RefreshCaptchaTokenInLogin(), so captcha/init requests no longer carry an expired bearer header that could trigger 4122 and be blocked by the guard clause.

P1 - Verified captcha token overwrite: After investigating how AnimeX handles this, we found that AnimeX's CaptchaTokenWithMeta() completely ignores resp.Url and proceeds with whatever captcha token is returned. OpenList's refreshCaptchaToken() checks resp.Url and halts with a "need verify" error, which is why users must manually verify.

Instead of conditionally preserving user-provided tokens, we added a new SkipVerification config option (default: true) that controls this behavior:

  • true (default): ignore resp.Url like AnimeX, enables fully automatic captcha handling
  • false: preserve original behavior, halt and prompt user for manual verification

P2 - Empty test bodies: Acknowledged. These are design-contract documentation tests. Happy to remove them if preferred, or replace with HTTP-mock integration tests if there is an existing mock pattern in the repo.

@pikachuren pikachuren left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 感谢 @wray-lee 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 Claude Opus 5 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。项目维护者会综合考虑代码质量、项目规划、技术方向、团队资源等多方面因素做出决策。

🎯 结论

🔄 Request Changes — 自动重登录很实用,但 SkipVerification 默认为 true 会默认绕过官方风控验证,需要重新考虑

📖 概要

feat(pikpak): auto re-login with username/password when tokens expire · 令牌失效时用账号密码自动重新登录。
核心改动:RefreshToken 改为非必填,登录成功后持久化令牌;新增 SkipVerification 开关;修复 captcha 相关的递归与签名参数缺失。

🧭 整体方案

技术路线是「令牌失效 → 用已保存的账号密码静默重登」,让用户不必手动更新 refresh token,出发点很好。实现里有几处质量不错的细节:请求失败分支加了 /v1/auth//v1/shield/captcha/ 的 URL 判断来阻断无限递归,登录前主动清空过期 AccessToken,令牌为空时显式报错——这些都说明作者仔细考虑过边界。主要顾虑集中在新增的 SkipVerification 开关上。

📊 变更统计

3 个文件(+218 / -11 行) | 功能 ⭐⭐⭐⭐ | 最小改动 ⭐⭐⭐ | 前向兼容 ⭐⭐⭐ | 方案设计 ⭐⭐⭐

🚨 关键问题

P0(阻塞合并)

  • ⚠️ drivers/pikpak/meta.go + util.go:430 — 新增 SkipVerification bool default:"true",配合 if resp.Url != "" && !d.Addition.SkipVerification 的判断,效果是:默认情况下,服务端返回需要人机验证时,代码直接忽略并继续。这有两个层面的问题:其一,服务端下发验证 URL 通常意味着触发了风控,强行继续可能导致账号被进一步限制甚至封禁;其二,把「绕过验证」作为默认开启的行为,改变了所有现有用户的既有语义。请问是否考虑至少把默认值改为 false,让用户显式选择承担风险呢?例如:
SkipVerification bool `json:"skip_verification" default:"false" help:"skip human verification prompt; may trigger risk control"`

P1(建议修复)

  • ⚠️ util.go:login() — 每次登录都无条件调用 RefreshCaptchaTokenInLogin,移除了原先「已有 captcha token 就复用」的判断。这在令牌频繁失效的场景下会显著增加 captcha 接口调用频次,本身也可能触发风控。请问这个改动是为了解决什么具体问题呢?如果是担心 token 过期,是否可以保留复用、仅在失败后再强制刷新?
  • ⚠️ RefreshTokenrequired:"true" 改为 required:"false",意味着可以只配账号密码。这降低了配置门槛,但也意味着密码成为唯一凭证并长期存储。请问在项目的存储加密策略下,driver Addition 中的密码字段是加密保存的吗?建议在 PR 描述中说明~

P2(可选)

  • 💡 login() 结尾新增 op.MustSaveDriverStorage(d),与 refreshToken() 中已有的调用形成两处持久化点。逻辑正确,但高频失效场景下会频繁写库,可留意~
  • 💡 新增的 util_test.go 覆盖了 GetActionGetCaptchaSign 等纯函数,测试质量不错,赞~
  • 💡 RefreshCaptchaTokenInLogin 补充 client_version / package_name / captcha_sign 三个 metas,这个修复看起来是对的(与官方客户端行为对齐),建议在描述里说明依据~

📂 逐文件分析

drivers/pikpak/meta.go

改动意图:放宽 RefreshToken 必填、新增跳过验证开关。
问题分析SkipVerification 默认值取向有风险(P0)。

drivers/pikpak/util.go

改动意图:实现自动重登录并修复 captcha 流程。
代码逻辑login() 清空旧 token → 强制刷新 captcha → 登录 → 校验非空 → 持久化;request() 在 auth/captcha 类 URL 上直接返回错误以断开递归。
问题分析递归阻断的处理是本 PR 的亮点——case 4122, 4121, 16case 9 都加了 URL 前缀判断,避免了刷新令牌失败时反复自调用,这个边界考虑得很好。空令牌校验也补得到位。问题集中在 SkipVerification(P0)与 captcha 无条件刷新(P1)。

drivers/pikpak/util_test.go

问题分析:纯函数测试,覆盖合理,无问题。

✅ 待处理清单

  • [P0] 将 SkipVerification 默认值改为 false,并在 help 文案中提示风控风险
  • [P1] 说明 captcha token 无条件刷新的动机,评估能否保留复用逻辑
  • [P1] 在描述中说明密码字段的存储与加密方式
  • [P2] 补充 captcha metas 新增字段的依据说明

🎯 结论:🔄 Request Changes — 自动重登录与递归阻断实现得不错,但默认绕过人机验证的取向需先调整。

Defaulting to true silently changed behaviour for existing storages,
since the resp.Url check in refreshCaptchaToken() is active on main.
It was also inconsistent with every other bypass-style boolean in
drivers/ (webdav.TlsInsecureSkipVerify, sftp.IgnoreSymlinkError,
doubao_new.IgnoreJWTCheck, 189pc.NoUseOcr, s3.ForcePathStyle), which
all default to false.

Also adds a help string naming the risk-control tradeoff, so users who
opt in know what they are accepting.

Co-Authored-By: Claude <noreply@anthropic.com>
@wray-lee

wray-lee commented Sep 1, 2026

Copy link
Copy Markdown
Author

Thanks for the review. I've changed the P0 item; for the rest I think the current behaviour is right, reasoning below.

P0 — SkipVerification default

Changed to default:"false" with a help string that names the risk:

SkipVerification bool `json:"skip_verification" default:"false" help:"ignore the human verification URL returned by the captcha API instead of failing; enabling this may trigger PikPak risk control"`

You're right about the backward-compat problem. The resp.Url != "" check in refreshCaptchaToken() is active on main, so shipping the option as default-true would have silently flipped semantics for every existing storage on upgrade. It's also out of line with the rest of the project — every other bypass-style boolean in drivers/ defaults to false (webdav.TlsInsecureSkipVerify, sftp.IgnoreSymlinkError, doubao_new.IgnoreJWTCheck, 189pc.NoUseOcr, s3.ForcePathStyle).

For context on why the option exists at all: /v1/shield/captcha/init can return a non-empty url together with a captcha_token that signin still accepts. Main treats that response as fatal, which makes unattended re-login impossible in that case. The implementation I compared against ignores the field and proceeds with the returned token. This makes that path available to users who want it without changing what anyone gets by default.

P1 — unconditional captcha refresh

I'd like to keep this. It's what the previous review asked for (point 3): "the login captcha should either be refreshed first, or signin should refresh it and retry once when a captcha-related error is returned." Refreshing first is the cheaper of the two branches — the signin request doesn't go through d.request(), so the case 9 recovery there can't reach it, and adding captcha-error detection plus a retry inside signin is more code for the same outcome.

On call frequency: login() has exactly two call sites — Init() when no refresh token is configured (driver.go:93), and refreshToken() when the refresh request returns 4126 (util.go:160). It isn't on the per-request path, so this costs one extra captcha call per re-login, not per API call.

P1 — password storage

Direct answer: driver Addition is stored as plaintext JSON. saveDriverStorage() in internal/op/storage.go marshals it with utils.Json.MarshalToString and writes it through GORM with no encryption step, and internal/model/storage.go declares the column as gorm:"type:text".

That's pre-existing and applies to every driver with credentials, not something this PR changes. Password required:"true" is already on main; the PR only drops the requirement to also paste a refresh token by hand. No new secret is stored. If addition-level encryption is something the project wants, that seems like its own change rather than part of this one.

P2 — two persistence points

refreshToken() already called op.MustSaveDriverStorage(d) on main; the PR adds one in login(). Since login() only runs at init-without-token or after a 4126, write frequency tracks re-logins rather than request volume. Happy to consolidate into a single call site if you'd prefer that.

P2 — captcha meta fields

client_version, package_name, timestamp and captcha_sign were added to match RefreshCaptchaTokenAtLogin, which already sends all four to the same endpoint on main. RefreshCaptchaTokenInLogin was sending only the email/phone/username meta, which is why captcha requests on the signin path behaved differently from post-login ones. The fix just brings the two in line.

CI

gh pr checks still reports no checks on this branch — the workflow runs are sitting in action_required because this is a fork PR. Could someone approve the run? I'd rather have CI verify the build and tests than have you take my word for it.

@jyxjjj

jyxjjj commented Sep 2, 2026

Copy link
Copy Markdown
Member

I've approved the CI runs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants