Add remote browser pages and website sync
Enable managed remote browser custom pages with login autofill and add website sync workflows so external admin surfaces can be handled inside SmartUp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -27,14 +27,42 @@ def _find_token(value: Any) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _find_user_id(value: Any) -> str:
|
||||
if isinstance(value, dict):
|
||||
for key in ("id", "user_id", "userId"):
|
||||
candidate = value.get(key)
|
||||
if candidate is not None:
|
||||
return str(candidate)
|
||||
for key in ("data", "result", "user", "session"):
|
||||
user_id = _find_user_id(value.get(key))
|
||||
if user_id:
|
||||
return user_id
|
||||
return ""
|
||||
|
||||
|
||||
def _unwrap_list(value: Any) -> Optional[list[dict[str, Any]]]:
|
||||
def _normalize(lst: list) -> list[dict[str, Any]]:
|
||||
out = []
|
||||
for i in lst:
|
||||
if isinstance(i, dict):
|
||||
out.append(i)
|
||||
elif isinstance(i, str):
|
||||
out.append({"id": i, "name": i})
|
||||
return out
|
||||
|
||||
if isinstance(value, list):
|
||||
return [i for i in value if isinstance(i, dict)]
|
||||
return _normalize(value)
|
||||
if isinstance(value, dict):
|
||||
for key in ("data", "items", "groups", "available_groups", "availableGroups"):
|
||||
nested = value.get(key)
|
||||
if isinstance(nested, list):
|
||||
return [i for i in nested if isinstance(i, dict)]
|
||||
return _normalize(nested)
|
||||
elif isinstance(nested, dict):
|
||||
# Handle /api/user/self/groups where data is a dict of group_name -> { desc, ratio }
|
||||
out = []
|
||||
for k in nested.keys():
|
||||
out.append({"id": k, "name": k})
|
||||
return out
|
||||
return None
|
||||
|
||||
|
||||
@@ -76,19 +104,59 @@ def _rate_from_group(group: dict[str, Any]) -> str:
|
||||
def _extract_rates_map(raw: Any) -> dict[str, str]:
|
||||
if raw is None:
|
||||
return {}
|
||||
|
||||
# Handle one-api/new-api /api/option response where GroupRatio is in a list of options
|
||||
if isinstance(raw, dict) and isinstance(raw.get("data"), list):
|
||||
for item in raw["data"]:
|
||||
if isinstance(item, dict) and item.get("key") == "GroupRatio":
|
||||
val = item.get("value")
|
||||
if isinstance(val, str):
|
||||
try:
|
||||
import json
|
||||
parsed = json.loads(val)
|
||||
if isinstance(parsed, dict):
|
||||
result: dict[str, str] = {}
|
||||
for k, v in parsed.items():
|
||||
r = _decimal_str(v)
|
||||
if r:
|
||||
result[str(k)] = r
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
elif isinstance(val, dict):
|
||||
# In case it's returned as dict directly
|
||||
result = {}
|
||||
for k, v in val.items():
|
||||
r = _decimal_str(v)
|
||||
if r:
|
||||
result[str(k)] = r
|
||||
return result
|
||||
|
||||
if isinstance(raw, dict):
|
||||
candidates = raw
|
||||
for key in ("data", "rates", "group_rates", "groupRates"):
|
||||
for key in ("data", "rates", "group_rates", "groupRates", "GroupRatio"):
|
||||
nested = raw.get(key)
|
||||
if isinstance(nested, dict):
|
||||
candidates = nested
|
||||
break
|
||||
elif isinstance(nested, str) and key == "GroupRatio":
|
||||
# Handle GroupRatio as a JSON string
|
||||
try:
|
||||
import json
|
||||
parsed = json.loads(nested)
|
||||
if isinstance(parsed, dict):
|
||||
candidates = parsed
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result: dict[str, str] = {}
|
||||
for k, v in candidates.items():
|
||||
if isinstance(v, dict):
|
||||
r = _decimal_str(
|
||||
v.get("rate_multiplier") or v.get("rateMultiplier")
|
||||
or v.get("user_rate_multiplier") or v.get("userRateMultiplier")
|
||||
or v.get("ratio")
|
||||
)
|
||||
else:
|
||||
r = _decimal_str(v)
|
||||
@@ -151,6 +219,8 @@ class UpstreamClient:
|
||||
self.auth_config = auth_config
|
||||
self.timeout = timeout
|
||||
self._token: str = ""
|
||||
self._cookies: dict[str, str] = {}
|
||||
self._new_api_user: str = ""
|
||||
|
||||
def _url(self, path: str) -> str:
|
||||
prefix = f"/{self.api_prefix}" if self.api_prefix else ""
|
||||
@@ -174,15 +244,29 @@ class UpstreamClient:
|
||||
headers[header] = key
|
||||
elif self.auth_type == "login_password" and self._token:
|
||||
headers["Authorization"] = f"Bearer {self._token}"
|
||||
if self.auth_type == "login_password" and self._new_api_user:
|
||||
headers["New-Api-User"] = self._new_api_user
|
||||
return headers
|
||||
|
||||
def _request(self, method: str, path: str, body: Any = None, auth: bool = True) -> Any:
|
||||
url = self._url(path)
|
||||
with httpx.Client(timeout=self.timeout) as client:
|
||||
if body is not None:
|
||||
resp = client.request(method, url, json=body, headers=self._headers(auth))
|
||||
resp = client.request(
|
||||
method,
|
||||
url,
|
||||
json=body,
|
||||
headers=self._headers(auth),
|
||||
cookies=self._cookies,
|
||||
)
|
||||
else:
|
||||
resp = client.request(method, url, headers=self._headers(auth))
|
||||
resp = client.request(
|
||||
method,
|
||||
url,
|
||||
headers=self._headers(auth),
|
||||
cookies=self._cookies,
|
||||
)
|
||||
self._cookies.update(dict(resp.cookies))
|
||||
resp.raise_for_status()
|
||||
ct = resp.headers.get("content-type", "")
|
||||
if not resp.content:
|
||||
@@ -198,13 +282,18 @@ class UpstreamClient:
|
||||
email = self.auth_config.get("email", "")
|
||||
password = self.auth_config.get("password", "")
|
||||
login_path = self.auth_config.get("login_path", "/auth/login")
|
||||
username_field = self.auth_config.get("username_field", "email")
|
||||
if not email or not password:
|
||||
raise UpstreamError("login_password auth requires email and password in auth_config")
|
||||
resp = self._request("POST", login_path, {"email": email, "password": password}, auth=False)
|
||||
resp = self._request("POST", login_path, {username_field: email, "password": password}, auth=False)
|
||||
token = _find_token(resp)
|
||||
if not token:
|
||||
raise UpstreamError("login succeeded but no token found in response")
|
||||
self._token = token
|
||||
if token:
|
||||
self._token = token
|
||||
return
|
||||
if self._cookies:
|
||||
self._new_api_user = self.auth_config.get("new_api_user", "") or _find_user_id(resp)
|
||||
return
|
||||
raise UpstreamError("login succeeded but no token or session cookie found in response")
|
||||
|
||||
def get_available_groups(self, endpoint: str) -> list[dict[str, Any]]:
|
||||
resp = self._request("GET", endpoint)
|
||||
|
||||
Reference in New Issue
Block a user