feat: remote browser login persistence + balance display + UI consistency
- Retain login state in remote browser profiles (don't delete on disconnect)
- Add GET /api/browser-sessions/{id}/clipboard for clipboard sync
- Add POST /api/browser-sessions/{id}/autofill-login for manual credential fill
- Add DELETE /api/browser-sessions/profiles/{custom_page_id} for login clear
- Add balance tracking with configurable divisor (balance_divisor)
- Health check on session reuse, idle TTL eviction, background cleanup
- Add first-frame watchdog (10s timeout) to prevent infinite loading
- Reconnect browser on active=true when session was closed
- UI: uniform text-only inline buttons (websites + upstreams pages)
- Fix page switch race with closingRemoteSessionPromise
This commit is contained in:
@@ -34,10 +34,18 @@ def _clean_auth_header_value(value: Any, field_name: str) -> str:
|
||||
return ""
|
||||
if text.startswith("Bearer "):
|
||||
text = text[7:].strip()
|
||||
# Try to sanitize non-latin-1 characters instead of hard-failing
|
||||
try:
|
||||
text.encode("latin-1")
|
||||
except UnicodeEncodeError as exc:
|
||||
raise UpstreamError(f"{field_name} contains non-HTTP-header characters; please re-extract and apply the full credential") from exc
|
||||
except UnicodeEncodeError:
|
||||
# Try stripping non-ASCII characters
|
||||
cleaned = text.encode("ascii", errors="ignore").decode("ascii").strip()
|
||||
if cleaned:
|
||||
return cleaned
|
||||
raise UpstreamError(
|
||||
f"{field_name} 含有非 HTTP 标头字符(如中文或 emoji),"
|
||||
f"请重新登录后再试"
|
||||
) from None
|
||||
return text
|
||||
|
||||
|
||||
@@ -325,3 +333,30 @@ class UpstreamClient:
|
||||
|
||||
def get_group_rates(self, endpoint: str) -> Any:
|
||||
return self._request("GET", endpoint)
|
||||
|
||||
def get_balance(self, endpoint: str, response_path: str) -> Optional[float]:
|
||||
"""Call the balance endpoint and extract a numeric value using a dot-separated JSON path.
|
||||
|
||||
response_path 示例:
|
||||
"balance" → resp["balance"]
|
||||
"data.quota" → resp["data"]["quota"]
|
||||
"data.total_balance" → resp["data"]["total_balance"]
|
||||
"""
|
||||
if not endpoint or not response_path:
|
||||
return None
|
||||
resp = self._request("GET", endpoint)
|
||||
if not isinstance(resp, dict):
|
||||
return None
|
||||
parts = response_path.split(".")
|
||||
value: Any = resp
|
||||
for part in parts:
|
||||
if isinstance(value, dict):
|
||||
value = value.get(part)
|
||||
else:
|
||||
return None
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user