{"record":{"id":"a6907a564b5b0bba","repo":"Wei-Shaw/sub2api","slug":"refresh-token-accesstoken","errorCode":null,"errorMessage":"未包含 refresh_token，且无法解析 accessToken 过期时间；请在第一步设置过期时间后再导入","messagePattern":"未包含 refresh_token，且无法解析 accessToken 过期时间；请在第一步设置过期时间后再导入","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/internal/handler/admin/account_codex_import.go","lineNumber":805,"sourceCode":"\t\tt := time.Unix(*req.ExpiresAt, 0).UTC()\n\t\trequestExpiresAt = &t\n\t}\n\n\tvar accountExpiresAt *time.Time\n\tvar credentialExpiresAt *time.Time\n\twarnings := make([]string, 0, 2)\n\tif item.RefreshToken == \"\" {\n\t\tif item.TokenExpiresAt != nil {\n\t\t\ttokenExpiresAt := item.TokenExpiresAt.UTC()\n\t\t\taccountExpiresAt = &tokenExpiresAt\n\t\t\tcredentialExpiresAt = &tokenExpiresAt\n\t\t}\n\t\tif requestExpiresAt != nil {\n\t\t\taccountExpiresAt = earlierCodexTime(accountExpiresAt, requestExpiresAt)\n\t\t\tcredentialExpiresAt = earlierCodexTime(credentialExpiresAt, requestExpiresAt)\n\t\t}\n\t\tif accountExpiresAt == nil {\n\t\t\treturn nil, nil, nil, nil, errors.New(\"未包含 refresh_token，且无法解析 accessToken 过期时间；请在第一步设置过期时间后再导入\")\n\t\t}\n\t\tif accountExpiresAt.Unix() <= time.Now().UTC().Unix()-codexImportClockSkewSeconds {\n\t\t\treturn nil, nil, nil, nil, fmt.Errorf(\"过期时间已过期: %s\", accountExpiresAt.Format(time.RFC3339))\n\t\t}\n\t\twarnings = append(warnings, \"未包含 refresh_token，已按 accessToken/账号过期时间设置自动停止调度\")\n\t\tif req.AutoPauseOnExpired != nil && !*req.AutoPauseOnExpired {\n\t\t\twarnings = append(warnings, \"未包含 refresh_token，已强制开启过期自动暂停\")\n\t\t}\n\t\tautoPause := true\n\t\texpiresAtUnix := accountExpiresAt.Unix()\n\t\treturn &expiresAtUnix, credentialExpiresAt, &autoPause, warnings, nil\n\t}\n\n\tif requestExpiresAt != nil {\n\t\taccountExpiresAt = requestExpiresAt\n\t}\n\tif item.TokenExpiresAt != nil {\n\t\ttokenExpiresAt := item.TokenExpiresAt.UTC()","sourceCodeStart":787,"sourceCodeEnd":823,"githubUrl":"https://github.com/Wei-Shaw/sub2api/blob/073e92d17178a1ccdb0a27017f572f10c9c7ab62/backend/internal/handler/admin/account_codex_import.go#L787-L823","documentation":"Returned by resolveCodexImportExpiry (backend/internal/handler/admin/account_codex_import.go:805) when an import entry has no refresh_token AND no expiry can be determined — neither a parseable exp claim from the access token (item.TokenExpiresAt is nil) nor a request-level req.ExpiresAt. Without any expiry the scheduler cannot auto-stop the account, so the import is refused and the admin is told to set an expiry in step one of the import flow.","triggerScenarios":"Importing a Codex auth.json lacking refresh_token where the accessToken is opaque (exp not parseable) and the import request's ExpiresAt field is unset or zero. The nearby branch also rejects entries whose resolved expiry is already past ('过期时间已过期').","commonSituations":"Short-lived demo tokens pasted without metadata; JWTs signed with an unreadable/expired exp; admin skipping the 'set expiry' step in the two-step import wizard; exporting tokens from a tool that strips refresh tokens.","solutions":["Set ExpiresAt (unix seconds) on the import request to the token's known expiry, then re-import.","Include a refresh_token in the entry so the account can renew and the expiry policy does not apply.","If the access token is a JWT, verify it still carries a valid exp claim (decode it) — if parseable, TokenExpiresAt is populated automatically and this error disappears.","If the token really has no refresh and unknown expiry, obtain a fresh token first."],"exampleFix":"// before\nPOST /admin/codex/import\n{ \"items\": [ { \"accessToken\": \"opaque-token-no-exp\" } ] }\n\n// after\nPOST /admin/codex/import\n{ \"expiresAt\": 1770000000, \"items\": [ { \"accessToken\": \"opaque-token-no-exp\" } ] }","handlingStrategy":"validation","validationCode":"function canResolveExpiry(entry: { refresh_token?: string; accessToken?: string }, reqExpiresAt?: number): boolean {\n  if (entry.refresh_token && entry.refresh_token.length > 0) return true\n  if (typeof reqExpiresAt === 'number' && reqExpiresAt > 0) return true\n  try {\n    const payload = JSON.parse(atob(entry.accessToken!.split('.')[1]))\n    return typeof payload.exp === 'number' && payload.exp > 0\n  } catch { return false }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["In the import wizard's first step, always set ExpiresAt when the entry has no refresh_token.","Prefer exports that include refresh_token so expiry policy is unnecessary.","Decode the JWT locally to confirm it carries an exp claim before importing opaque tokens.","Note the related failure '过期时间已过期' when the resolved expiry is already in the past (minus clock skew)."],"tags":["admin","import","expiry","refresh-token","validation","backend","codex"],"backgroundTag":null,"analyzedSha":"073e92d17178a1ccdb0a27017f572f10c9c7ab62","analyzedAt":"2026-08-15T14:33:00.750Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}