Fix browser PKCE stuck on consent without code.
Use real Allow clicks (JS click breaks React action), then browser-side Next.js Server Action POST as fallback; harden SSO cookie injection.
This commit is contained in:
1 parent
96061cbd78
commit
bf30d40c8b
1 file changed
+346
-96
+346
-96
@@ -563,21 +563,67 @@ def _ensure_sso_on_page(page: Any, sso: str, log: LogFn) -> None:
|
|||||||
if not sso:
|
if not sso:
|
||||||
raise OAuthCodeError("sso cookie 为空")
|
raise OAuthCodeError("sso cookie 为空")
|
||||||
# 先落到 accounts 域,再写 cookie,避免 set 失败
|
# 先落到 accounts 域,再写 cookie,避免 set 失败
|
||||||
try:
|
for url in ("https://accounts.x.ai/", "https://auth.x.ai/"):
|
||||||
page.get("https://accounts.x.ai/")
|
try:
|
||||||
time.sleep(0.4)
|
page.get(url)
|
||||||
except Exception as exc: # noqa: BLE001
|
time.sleep(0.25)
|
||||||
log(f"open accounts.x.ai warn: {exc}")
|
except Exception as exc: # noqa: BLE001
|
||||||
|
log(f"open {url} warn: {exc}")
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for domain in (".x.ai", "accounts.x.ai", ".accounts.x.ai", "auth.x.ai", ".auth.x.ai"):
|
||||||
|
for name in ("sso", "sso-rw"):
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"value": sso,
|
||||||
|
"domain": domain,
|
||||||
|
"path": "/",
|
||||||
|
"secure": True,
|
||||||
|
"sameSite": "None",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
set_ok = False
|
||||||
|
# 优先 CDP/DrissionPage set.cookies(可写 httpOnly 域 cookie)
|
||||||
|
for target in (page, getattr(page, "browser", None)):
|
||||||
|
if target is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
setter = getattr(target, "set", None)
|
||||||
|
cookies_fn = getattr(setter, "cookies", None) if setter is not None else None
|
||||||
|
if not callable(cookies_fn):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
cookies_fn(items)
|
||||||
|
set_ok = True
|
||||||
|
log(f"browser sso cookie set bulk via {type(target).__name__}")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
n = 0
|
||||||
|
for it in items:
|
||||||
|
try:
|
||||||
|
cookies_fn(it)
|
||||||
|
n += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if n:
|
||||||
|
set_ok = True
|
||||||
|
log(f"browser sso cookie set one-by-one={n}")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# document.cookie 兜底(非 httpOnly)
|
||||||
set_js = r"""
|
set_js = r"""
|
||||||
(sso) => {
|
(sso) => {
|
||||||
try {
|
try {
|
||||||
const maxAge = 60 * 60 * 24 * 30;
|
const maxAge = 60 * 60 * 24 * 30;
|
||||||
const base = `; path=/; max-age=${maxAge}; SameSite=Lax`;
|
const base = `; path=/; max-age=${maxAge}; SameSite=None; Secure`;
|
||||||
|
document.cookie = `sso=${sso}${base}; domain=.x.ai`;
|
||||||
|
document.cookie = `sso-rw=${sso}${base}; domain=.x.ai`;
|
||||||
document.cookie = `sso=${sso}${base}`;
|
document.cookie = `sso=${sso}${base}`;
|
||||||
document.cookie = `sso-rw=${sso}${base}`;
|
document.cookie = `sso-rw=${sso}${base}`;
|
||||||
// 兼容 secure 场景
|
|
||||||
document.cookie = `sso=${sso}${base}; Secure`;
|
|
||||||
document.cookie = `sso-rw=${sso}${base}; Secure`;
|
|
||||||
return document.cookie.includes('sso=');
|
return document.cookie.includes('sso=');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return String(e);
|
return String(e);
|
||||||
@@ -586,75 +632,232 @@ def _ensure_sso_on_page(page: Any, sso: str, log: LogFn) -> None:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
ok = _page_eval(page, set_js, sso)
|
ok = _page_eval(page, set_js, sso)
|
||||||
log(f"browser sso cookie set: {ok!r}")
|
log(f"browser sso document.cookie: {ok!r}")
|
||||||
|
set_ok = set_ok or (ok is True) or (ok == True) or (str(ok).lower() == "true")
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
log(f"browser sso document.cookie fail: {exc}")
|
||||||
|
|
||||||
|
if not set_ok:
|
||||||
|
raise OAuthCodeError("写入 sso cookie 失败")
|
||||||
|
|
||||||
|
|
||||||
|
def _page_html(page: Any) -> str:
|
||||||
|
try:
|
||||||
|
html = str(getattr(page, "html", "") or "")
|
||||||
|
if html:
|
||||||
|
return html
|
||||||
except Exception:
|
except Exception:
|
||||||
# 退而求其次:DrissionPage set.cookies
|
pass
|
||||||
try:
|
try:
|
||||||
setter = getattr(page, "set", None)
|
return str(_page_eval(page, "() => document.documentElement.outerHTML") or "")
|
||||||
cookies = getattr(setter, "cookies", None) if setter is not None else None
|
except Exception:
|
||||||
if callable(cookies):
|
return ""
|
||||||
for domain in ("accounts.x.ai", "auth.x.ai", ".x.ai"):
|
|
||||||
cookies({"name": "sso", "value": sso, "domain": domain, "path": "/"})
|
|
||||||
cookies({"name": "sso-rw", "value": sso, "domain": domain, "path": "/"})
|
def _extract_next_action_id(html: str) -> str:
|
||||||
log("browser sso cookie set via page.set.cookies")
|
"""从 consent 页 HTML 抽 Next-Action / Server Action id。"""
|
||||||
else:
|
text = html or ""
|
||||||
raise OAuthCodeError("无法写入 sso cookie")
|
patterns = (
|
||||||
except Exception as exc: # noqa: BLE001
|
r'"next-action"\s*:\s*"([a-f0-9]{20,})"',
|
||||||
raise OAuthCodeError(f"写入 sso cookie 失败: {exc}") from exc
|
r'"actionId"\s*:\s*"([a-f0-9]{20,})"',
|
||||||
|
r'\$ACTION_ID_([a-f0-9]{20,})',
|
||||||
|
r'next-action["\']?\s*[:=]\s*["\']([a-f0-9]{20,})',
|
||||||
|
)
|
||||||
|
for pat in patterns:
|
||||||
|
m = re.search(pat, text, re.I)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
return NEXT_ACTION_ID
|
||||||
|
|
||||||
|
|
||||||
def _browser_click_allow(page: Any, log: LogFn) -> bool:
|
def _browser_click_allow(page: Any, log: LogFn) -> bool:
|
||||||
js = r"""
|
"""Consent「允许」必须真实点击(JS click 会导致 Invalid action)。"""
|
||||||
() => {
|
labels = ("允许", "Allow", "Authorize", "Approve", "授权")
|
||||||
function isVisible(node) {
|
# 1) DrissionPage 真实点击(与 device consent 同一套经验)
|
||||||
if (!node) return false;
|
|
||||||
const style = window.getComputedStyle(node);
|
|
||||||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
|
||||||
const rect = node.getBoundingClientRect();
|
|
||||||
return rect.width > 0 && rect.height > 0;
|
|
||||||
}
|
|
||||||
function textOf(node) {
|
|
||||||
return [node.innerText, node.textContent, node.getAttribute('aria-label'), node.getAttribute('value')]
|
|
||||||
.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
|
||||||
}
|
|
||||||
const nodes = Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"], a'));
|
|
||||||
const prefer = [];
|
|
||||||
const weak = [];
|
|
||||||
for (const n of nodes) {
|
|
||||||
if (!isVisible(n) || n.disabled || n.getAttribute('aria-disabled') === 'true') continue;
|
|
||||||
const t = textOf(n);
|
|
||||||
const compact = t.replace(/\s+/g, '');
|
|
||||||
const lower = compact.toLowerCase();
|
|
||||||
if (!compact) continue;
|
|
||||||
// 精确允许,排除“全部允许”
|
|
||||||
if (compact === '允许' || lower === 'allow' || lower === 'authorize' || compact === '授权') {
|
|
||||||
prefer.push(n);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ((compact.includes('允许') || lower.includes('allow') || lower.includes('authorize'))
|
|
||||||
&& !compact.includes('全部') && !lower.includes('all')) {
|
|
||||||
weak.push(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const target = prefer[0] || weak[0];
|
|
||||||
if (!target) return {clicked:false, texts: nodes.slice(0,8).map(textOf)};
|
|
||||||
target.focus();
|
|
||||||
target.click();
|
|
||||||
return {clicked:true, text: textOf(target)};
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
ret = _page_eval(page, js)
|
candidates = []
|
||||||
|
for sel in ("tag:button", "css:button", "css:[role='button']", "css:input[type='submit']"):
|
||||||
|
try:
|
||||||
|
found = page.eles(sel, timeout=0.3) or []
|
||||||
|
except Exception:
|
||||||
|
found = []
|
||||||
|
for el in found:
|
||||||
|
try:
|
||||||
|
t = (getattr(el, "text", None) or el.raw_text or "").strip()
|
||||||
|
except Exception:
|
||||||
|
t = ""
|
||||||
|
if not t:
|
||||||
|
continue
|
||||||
|
compact = re.sub(r"\s+", "", t)
|
||||||
|
if compact in labels or t in labels:
|
||||||
|
candidates.append((0, el, t))
|
||||||
|
elif any(x in compact for x in labels) and "全部" not in compact and "All" not in compact:
|
||||||
|
candidates.append((1, el, t))
|
||||||
|
candidates.sort(key=lambda x: x[0])
|
||||||
|
for _, el, t in candidates:
|
||||||
|
try:
|
||||||
|
el.click() # real click
|
||||||
|
log(f"browser REAL click allow: {t!r}")
|
||||||
|
return True
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
log(f"real click {t!r} failed: {exc}")
|
||||||
|
try:
|
||||||
|
el.click(by_js=True)
|
||||||
|
log(f"browser JS click allow: {t!r}")
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
log(f"ele allow click failed: {exc}")
|
||||||
|
|
||||||
|
# 2) 表单 action=allow 后 submit(device consent 兜底同款)
|
||||||
|
try:
|
||||||
|
ret = _page_eval(
|
||||||
|
page,
|
||||||
|
r"""
|
||||||
|
() => {
|
||||||
|
const labels = new Set(['允许','Allow','Authorize','Approve','授权']);
|
||||||
|
const f = document.querySelector('form');
|
||||||
|
if (f) {
|
||||||
|
let a = f.querySelector('input[name=action]');
|
||||||
|
if (!a) {
|
||||||
|
a = document.createElement('input');
|
||||||
|
a.type = 'hidden';
|
||||||
|
a.name = 'action';
|
||||||
|
f.appendChild(a);
|
||||||
|
}
|
||||||
|
a.value = 'allow';
|
||||||
|
const btn = [...f.querySelectorAll('button,[role=button],input[type=submit]')]
|
||||||
|
.find(b => labels.has(((b.innerText||b.value||'').trim())));
|
||||||
|
if (btn) { btn.click(); return {ok:true, via:'form-btn'}; }
|
||||||
|
f.submit();
|
||||||
|
return {ok:true, via:'form-submit'};
|
||||||
|
}
|
||||||
|
// 无 form:真实派发 pointer/mouse 事件
|
||||||
|
const nodes = [...document.querySelectorAll('button,[role=button],input[type=submit],a')];
|
||||||
|
const target = nodes.find(n => {
|
||||||
|
const t = ((n.innerText||n.textContent||n.value||'').replace(/\s+/g,'')).trim();
|
||||||
|
return labels.has(t) || (t.includes('允许') && !t.includes('全部'));
|
||||||
|
});
|
||||||
|
if (!target) {
|
||||||
|
return {ok:false, texts: nodes.slice(0,10).map(n => (n.innerText||n.value||'').trim()).filter(Boolean)};
|
||||||
|
}
|
||||||
|
target.scrollIntoView({block:'center'});
|
||||||
|
target.focus();
|
||||||
|
for (const type of ['pointerdown','mousedown','pointerup','mouseup','click']) {
|
||||||
|
target.dispatchEvent(new MouseEvent(type, {bubbles:true, cancelable:true, view:window}));
|
||||||
|
}
|
||||||
|
return {ok:true, via:'mouse-events', text:(target.innerText||target.value||'').trim()};
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
if isinstance(ret, dict) and ret.get("ok"):
|
||||||
|
log(f"browser allow fallback: {ret}")
|
||||||
|
return True
|
||||||
|
log(f"browser allow button not found: {ret!r}")
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
log(f"click allow js failed: {exc}")
|
log(f"click allow js failed: {exc}")
|
||||||
return False
|
|
||||||
if isinstance(ret, dict) and ret.get("clicked"):
|
|
||||||
log(f"browser clicked allow: {ret.get('text')!r}")
|
|
||||||
return True
|
|
||||||
log(f"browser allow button not found: {ret!r}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _browser_consent_server_action(
|
||||||
|
page: Any,
|
||||||
|
consent_url: str,
|
||||||
|
flow: AuthCodeFlow,
|
||||||
|
log: LogFn,
|
||||||
|
) -> str:
|
||||||
|
"""在浏览器内 POST Next.js Server Action 批准 consent,直接拿 code。"""
|
||||||
|
html = _page_html(page)
|
||||||
|
action_id = _extract_next_action_id(html)
|
||||||
|
payload = [
|
||||||
|
{
|
||||||
|
"action": "allow",
|
||||||
|
"clientId": CLIENT_ID,
|
||||||
|
"redirectUri": REDIRECT_URI,
|
||||||
|
"scope": SCOPE,
|
||||||
|
"state": flow.state,
|
||||||
|
"codeChallenge": flow.code_challenge,
|
||||||
|
"codeChallengeMethod": "S256",
|
||||||
|
"nonce": flow.nonce,
|
||||||
|
"principalType": "User",
|
||||||
|
"principalId": "",
|
||||||
|
"referrer": GROK_REFERRER,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
body = json.dumps(payload, separators=(",", ":"))
|
||||||
|
# 用 window key 轮询,兼容 DrissionPage 对 Promise 支持不一
|
||||||
|
wrap = f"""
|
||||||
|
(() => {{
|
||||||
|
const key = '__cpa_consent_' + Date.now();
|
||||||
|
window[key] = null;
|
||||||
|
fetch({consent_url!r}, {{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {{
|
||||||
|
'Accept': 'text/x-component',
|
||||||
|
'Content-Type': 'text/plain;charset=UTF-8',
|
||||||
|
'Next-Action': {action_id!r},
|
||||||
|
'Origin': 'https://accounts.x.ai',
|
||||||
|
'Referer': {consent_url!r},
|
||||||
|
}},
|
||||||
|
body: {body!r},
|
||||||
|
credentials: 'include',
|
||||||
|
redirect: 'follow',
|
||||||
|
}}).then(async (r) => {{
|
||||||
|
const text = await r.text();
|
||||||
|
window[key] = {{status: r.status, url: r.url, text: text}};
|
||||||
|
}}).catch((e) => {{
|
||||||
|
window[key] = {{status: 0, url: '', text: String(e)}};
|
||||||
|
}});
|
||||||
|
return key;
|
||||||
|
}})()
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
key = _page_eval(page, wrap)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
raise OAuthCodeError(f"browser consent fetch 启动失败: {exc}") from exc
|
||||||
|
log(f"browser consent server-action post action_id={action_id[:12]}…")
|
||||||
|
deadline = time.time() + 20
|
||||||
|
ret = None
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
ret = _page_eval(page, f"() => window[{key!r}]")
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
ret = _page_eval(page, f"window[{key!r}]")
|
||||||
|
except Exception:
|
||||||
|
ret = None
|
||||||
|
if ret:
|
||||||
|
break
|
||||||
|
time.sleep(0.2)
|
||||||
|
if not isinstance(ret, dict):
|
||||||
|
raise OAuthCodeError(f"browser consent 无响应: {ret!r}")
|
||||||
|
status = int(ret.get("status") or 0)
|
||||||
|
text = str(ret.get("text") or "")
|
||||||
|
final = str(ret.get("url") or "")
|
||||||
|
if status and (status < 200 or status >= 300):
|
||||||
|
raise OAuthCodeError(f"browser consent HTTP {status}: {_short(text, 240)}")
|
||||||
|
if "code=" in final:
|
||||||
|
qs = parse_qs(urlparse(final).query)
|
||||||
|
code = (qs.get("code") or [""])[0].strip()
|
||||||
|
if code:
|
||||||
|
log("browser got code from consent redirect url")
|
||||||
|
return code
|
||||||
|
# 响应体 / RSC
|
||||||
|
try:
|
||||||
|
code = parse_consent_code(text)
|
||||||
|
if code:
|
||||||
|
log("browser got code from consent server-action body")
|
||||||
|
return code
|
||||||
|
except OAuthCodeError:
|
||||||
|
pass
|
||||||
|
# 有时 code 在 text 的 redirect 串里
|
||||||
|
m = re.search(r"[?&]code=([A-Za-z0-9._~\-]+)", text)
|
||||||
|
if m:
|
||||||
|
log("browser got code from consent body regex")
|
||||||
|
return m.group(1)
|
||||||
|
raise OAuthCodeError(f"browser consent 未返回 code: {_short(text, 240)}")
|
||||||
|
|
||||||
|
|
||||||
def _browser_fetch_token(page: Any, code: str, flow: AuthCodeFlow, log: LogFn) -> TokenResult:
|
def _browser_fetch_token(page: Any, code: str, flow: AuthCodeFlow, log: LogFn) -> TokenResult:
|
||||||
"""在浏览器上下文用 fetch 换 token,绕开 Python TLS 对 auth.x.ai 的 EOF。"""
|
"""在浏览器上下文用 fetch 换 token,绕开 Python TLS 对 auth.x.ai 的 EOF。"""
|
||||||
form = {
|
form = {
|
||||||
@@ -833,8 +1036,22 @@ def mint_from_sso_browser(
|
|||||||
raise OAuthCodeError(f"browser 打开 authorize 失败: {exc}") from exc
|
raise OAuthCodeError(f"browser 打开 authorize 失败: {exc}") from exc
|
||||||
|
|
||||||
code = ""
|
code = ""
|
||||||
deadline = time.time() + max(20.0, float(timeout_sec))
|
deadline = time.time() + max(30.0, float(timeout_sec))
|
||||||
last_url = ""
|
last_url = ""
|
||||||
|
consent_attempts = 0
|
||||||
|
server_action_tried = False
|
||||||
|
wait_after_click_until = 0.0
|
||||||
|
|
||||||
|
def _code_from_url(u: str) -> str:
|
||||||
|
if not u or "code=" not in u:
|
||||||
|
return ""
|
||||||
|
if not any(x in u for x in ("127.0.0.1", "localhost", "callback", "redirect")):
|
||||||
|
# 仍允许任意带 code 的 redirect
|
||||||
|
if "code=" not in u:
|
||||||
|
return ""
|
||||||
|
qs = parse_qs(urlparse(u).query)
|
||||||
|
return (qs.get("code") or [""])[0].strip()
|
||||||
|
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
try:
|
try:
|
||||||
url = str(getattr(page, "url", "") or "")
|
url = str(getattr(page, "url", "") or "")
|
||||||
@@ -845,38 +1062,59 @@ def mint_from_sso_browser(
|
|||||||
last_url = url
|
last_url = url
|
||||||
|
|
||||||
# callback 已跳到 redirect_uri?code=
|
# callback 已跳到 redirect_uri?code=
|
||||||
if "code=" in url and ("127.0.0.1" in url or "callback" in url or "localhost" in url):
|
code = _code_from_url(url)
|
||||||
qs = parse_qs(urlparse(url).query)
|
if code:
|
||||||
code = (qs.get("code") or [""])[0].strip()
|
log("browser got code from redirect")
|
||||||
if code:
|
break
|
||||||
log("browser got code from redirect")
|
|
||||||
break
|
|
||||||
|
|
||||||
# consent 页
|
# consent 页:真实点击 Allow;点后短暂等待跳转;仍不行再 Server Action POST
|
||||||
if "/oauth2/consent" in url or "consent" in url:
|
if "/oauth2/consent" in url or "/consent" in url:
|
||||||
_browser_click_allow(page, log)
|
# 刚点过,先等 redirect
|
||||||
time.sleep(0.8)
|
if wait_after_click_until and time.time() < wait_after_click_until:
|
||||||
# 有时 consent 响应是 RSC,不跳转;尝试从 HTML 抽 code
|
time.sleep(0.3)
|
||||||
try:
|
continue
|
||||||
html = ""
|
|
||||||
try:
|
consent_attempts += 1
|
||||||
html = str(getattr(page, "html", "") or "")
|
if consent_attempts <= 4:
|
||||||
except Exception:
|
clicked = _browser_click_allow(page, log)
|
||||||
html = str(_page_eval(page, "() => document.documentElement.outerHTML") or "")
|
if clicked:
|
||||||
if html:
|
wait_after_click_until = time.time() + 4.0
|
||||||
|
time.sleep(0.6)
|
||||||
|
# 点后立刻看 url / html
|
||||||
try:
|
try:
|
||||||
code = parse_consent_code(html)
|
url2 = str(getattr(page, "url", "") or "")
|
||||||
if code:
|
except Exception:
|
||||||
log("browser got code from consent html")
|
url2 = url
|
||||||
break
|
code = _code_from_url(url2)
|
||||||
except OAuthCodeError:
|
if code:
|
||||||
pass
|
log("browser got code after allow click")
|
||||||
except Exception:
|
break
|
||||||
pass
|
html = _page_html(page)
|
||||||
|
if html:
|
||||||
|
try:
|
||||||
|
code = parse_consent_code(html)
|
||||||
|
if code:
|
||||||
|
log("browser got code from consent html after click")
|
||||||
|
break
|
||||||
|
except OAuthCodeError:
|
||||||
|
pass
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 真实点击无效:浏览器内 Server Action POST(与 HTTP approve 同 payload)
|
||||||
|
if not server_action_tried:
|
||||||
|
server_action_tried = True
|
||||||
|
try:
|
||||||
|
code = _browser_consent_server_action(page, url, flow, log)
|
||||||
|
if code:
|
||||||
|
break
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
log(f"browser consent server-action failed: {exc}")
|
||||||
|
time.sleep(0.5)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if "sign-in" in url or "sign-up" in url:
|
if "sign-in" in url or "sign-up" in url:
|
||||||
# cookie 可能没带上,重写一次
|
# cookie 可能没带上,重写一次
|
||||||
|
log("browser landed sign-in, re-inject sso")
|
||||||
_ensure_sso_on_page(page, sso, log)
|
_ensure_sso_on_page(page, sso, log)
|
||||||
try:
|
try:
|
||||||
page.get(auth_url)
|
page.get(auth_url)
|
||||||
@@ -885,8 +1123,20 @@ def mint_from_sso_browser(
|
|||||||
time.sleep(0.8)
|
time.sleep(0.8)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
time.sleep(0.5)
|
# 中间跳转页:稍等
|
||||||
|
time.sleep(0.4)
|
||||||
|
|
||||||
|
if not code:
|
||||||
|
# 最后再扫一次页面 HTML
|
||||||
|
try:
|
||||||
|
html = _page_html(page)
|
||||||
|
if html:
|
||||||
|
try:
|
||||||
|
code = parse_consent_code(html)
|
||||||
|
except OAuthCodeError:
|
||||||
|
code = ""
|
||||||
|
except Exception:
|
||||||
|
code = ""
|
||||||
if not code:
|
if not code:
|
||||||
raise OAuthCodeError(
|
raise OAuthCodeError(
|
||||||
f"browser PKCE 超时未拿到 code(last_url={_short(last_url, 160)})"
|
f"browser PKCE 超时未拿到 code(last_url={_short(last_url, 160)})"
|
||||||
|
|||||||
Reference in new issue
Block a user