Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions api/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,14 @@ class AccountExportRequest(BaseModel):

class AccountUpdateRequest(BaseModel):
access_token: str = ""
account_id: str | None = None
row_id: str | None = None
type: str | None = None
provider: str | None = None
target_provider: Literal["gpt", "grok", "gemini"] | None = None
status: str | None = None
quota: int | None = None
proxy: str | None = None


class CPAPoolCreateRequest(BaseModel):
Expand Down Expand Up @@ -241,7 +244,7 @@ def _validate_gemini_import_payloads(tokens: list[str], payloads: list[dict[str,
if not payloads:
raise HTTPException(status_code=400, detail={"error": "Gemini 目前只支持 Cookie 双字段导入"})
top_level_gemini = normalize_provider(provider) == GEMINI_PROVIDER
allowed_keys = {"provider", "__Secure-1PSID", "__Secure-1PSIDTS"}
allowed_keys = {"provider", "__Secure-1PSID", "__Secure-1PSIDTS", "proxy"}
for item in payloads:
item_provider = str(item.get("provider") or "").strip()
if top_level_gemini and item_provider and normalize_account_provider(item_provider) != GEMINI_PROVIDER:
Expand Down Expand Up @@ -285,13 +288,20 @@ def _grok_import_requested(provider: str | None, payloads: list[dict[str, Any]])
def _validate_grok_import_payloads(tokens: list[str], payloads: list[dict[str, Any]], provider: str | None) -> list[str]:
if not _grok_import_requested(provider, payloads):
return tokens
if payloads:
raise HTTPException(status_code=400, detail={"error": "Grok 导入只接受裸 SSO 值,或每行一个 sso=<值>;不支持 sso-rw、完整 Cookie header、JSON、CPA、cookies 或 accounts 账号 payload"})
if not tokens:
if not tokens and not payloads:
raise HTTPException(status_code=400, detail={"error": "Grok 导入只接受裸 SSO 值,或每行一个 sso=<值>"})
allowed_keys = {"provider", "sso", "proxy"}
for item in payloads:
item_provider = str(item.get("provider") or provider or "").strip()
if item_provider and normalize_account_provider(item_provider) != GROK_PROVIDER:
raise HTTPException(status_code=400, detail={"error": "Grok 导入不能混用其他供应商账号"})
extra_keys = {key for key, value in item.items() if value is not None} - allowed_keys
sso = str(item.get("sso") or "").strip()
if extra_keys or not _normalize_grok_sso_import_token(sso):
raise HTTPException(status_code=400, detail={"error": "Grok 导入只接受裸 SSO 值,或每行一个 sso=<值>;不支持 sso-rw、完整 Cookie header、其他 Cookie 名称、JSON、CPA 或 cookies"})
normalized_tokens = [_normalize_grok_sso_import_token(token) for token in tokens]
if not all(normalized_tokens):
raise HTTPException(status_code=400, detail={"error": "Grok 导入只接受裸 SSO 值,或每行一个 sso=<值>;不支持 sso-rw、完整 Cookie header、其他 Cookie 名称、JSON、CPA、cookiesaccounts 账号 payload"})
raise HTTPException(status_code=400, detail={"error": "Grok 导入只接受裸 SSO 值,或每行一个 sso=<值>;不支持 sso-rw、完整 Cookie header、其他 Cookie 名称、JSON、CPA 或 cookies"})
return normalized_tokens


Expand Down Expand Up @@ -480,21 +490,26 @@ async def export_accounts(body: AccountExportRequest, authorization: str | None
async def update_account(body: AccountUpdateRequest, authorization: str | None = Header(default=None)):
require_admin(authorization)
access_token = str(body.access_token or "").strip()
if not access_token:
raise HTTPException(status_code=400, detail={"error": "access_token is required"})
identifier = _delete_identifiers([{"account_id": body.account_id, "row_id": body.row_id}])
if not access_token and not identifier:
raise HTTPException(status_code=400, detail={"error": "access_token or identifier is required"})
updates = {
key: value
for key, value in {
"type": body.type,
"provider": body.provider,
"status": body.status,
"quota": body.quota,
"proxy": body.proxy,
}.items()
if value is not None
}
if not updates:
raise HTTPException(status_code=400, detail={"error": "还没有检测到改动,请修改后再保存"})
account = account_service.update_account(access_token, updates, provider=body.target_provider)
if access_token:
account = account_service.update_account(access_token, updates, provider=body.target_provider)
else:
account = account_service.update_account_by_identifier(identifier[0], updates, provider=body.target_provider)
if account is None:
raise HTTPException(status_code=404, detail={"error": "account not found"})
return {"item": sanitize_account(account), "items": sanitize_accounts(account_service.list_accounts(provider=body.target_provider))}
Expand Down
46 changes: 30 additions & 16 deletions services/account_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ def _normalize_account(self, item: dict) -> dict | None:
normalized["success"] = int(normalized.get("success") or 0)
normalized["fail"] = int(normalized.get("fail") or 0)
normalized["last_used_at"] = normalized.get("last_used_at")
normalized["proxy"] = _clean_string(normalized.get("proxy"))
return normalized

def list_tokens(self, provider: str | None = None) -> list[str]:
Expand Down Expand Up @@ -824,27 +825,40 @@ def delete_limited_accounts(self, provider: str | None = None) -> dict:
items = self._list_account_items_locked(provider)
return {"removed": removed, "items": items}

def _update_account_locked(self, access_token: str, updates: dict, provider: str | None = None) -> dict | None:
current = self._get_account_locked(access_token, provider)
if current is None:
return None
account = self._normalize_account({**current, **updates, "access_token": access_token})
if account is None:
return None
if account.get("status") == "限流" and config.auto_remove_rate_limited_accounts:
self._pop_account_locked(access_token, provider)
self._save_accounts()
log_service.add(LOG_TYPE_ACCOUNT, "自动移除限流账号", {"token": anonymize_token(access_token)})
return None
self._set_account_locked(account)
self._save_accounts()
log_service.add(LOG_TYPE_ACCOUNT, "更新账号", {"token": anonymize_token(access_token), "status": account.get("status")})
return dict(account)

def update_account(self, access_token: str, updates: dict, provider: str | None = None) -> dict | None:
if not access_token:
return None
with self._lock:
current = self._get_account_locked(access_token, provider)
if current is None:
return None
account = self._normalize_account({**current, **updates, "access_token": access_token})
if account is None:
return None
if account.get("status") == "限流" and config.auto_remove_rate_limited_accounts:
self._pop_account_locked(access_token, provider)
self._save_accounts()
log_service.add(LOG_TYPE_ACCOUNT, "自动移除限流账号", {"token": anonymize_token(access_token)})
return self._update_account_locked(access_token, updates, provider)

def update_account_by_identifier(self, identifier: dict[str, str], updates: dict, provider: str | None = None) -> dict | None:
target_provider = self._provider_filter(provider)
if target_provider is None:
return None
with self._lock:
provider_accounts = self._accounts.get(target_provider, {})
matched_tokens = _matched_account_tokens_by_identifiers([identifier], provider_accounts, target_provider)
if len(matched_tokens) != 1:
return None
self._set_account_locked(account)
self._save_accounts()
log_service.add(LOG_TYPE_ACCOUNT, "更新账号",
{"token": anonymize_token(access_token), "status": account.get("status")})
return dict(account)
return None
access_token = next(iter(matched_tokens))
return self._update_account_locked(access_token, updates, target_provider)

def mark_image_result(self, access_token: str, success: bool) -> dict | None:
if not access_token:
Expand Down
9 changes: 5 additions & 4 deletions services/network/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

from typing import Any

def build_session_kwargs(*, impersonate: str | None = None, verify: bool = True, **session_kwargs: Any) -> dict[str, object]:

def build_session_kwargs(*, impersonate: str | None = None, verify: bool = True, account: dict | None = None, **session_kwargs: Any) -> dict[str, object]:
if impersonate:
session_kwargs["impersonate"] = impersonate
session_kwargs["verify"] = verify
from services.proxy_service import proxy_settings

return proxy_settings.build_session_kwargs(**session_kwargs)
return proxy_settings.build_session_kwargs(account=account, **session_kwargs)


def create_session(*, impersonate: str | None = None, verify: bool = True, **session_kwargs: Any):
def create_session(*, impersonate: str | None = None, verify: bool = True, account: dict | None = None, **session_kwargs: Any):
from curl_cffi import requests

return requests.Session(**build_session_kwargs(impersonate=impersonate, verify=verify, **session_kwargs))
return requests.Session(**build_session_kwargs(impersonate=impersonate, verify=verify, account=account, **session_kwargs))
10 changes: 7 additions & 3 deletions services/openai_backend_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,15 @@ def __init__(self, access_token: str = "") -> None:
self.client_version = DEFAULT_CLIENT_VERSION
self.client_build_number = DEFAULT_CLIENT_BUILD_NUMBER
self.access_token = access_token
self.account = self._load_account()
self.network_profile = self._build_network_profile()
self.fp = self.network_profile.as_fingerprint()
self.user_agent = self.fp["user-agent"]
self.device_id = self.fp["oai-device-id"]
self.session_id = self.fp["oai-session-id"]
self.pow_script_sources: list[str] = []
self.pow_data_build = ""
self.session = create_session(impersonate=self.network_profile.impersonate, verify=self.network_profile.verify)
self.session = create_session(impersonate=self.network_profile.impersonate, verify=self.network_profile.verify, account=self.account)
self.session.headers.update(build_chatgpt_web_headers(
self.network_profile,
base_url=self.base_url,
Expand All @@ -105,9 +106,12 @@ def __enter__(self) -> "OpenAIBackendAPI":
def __exit__(self, exc_type: object, exc: object, traceback: object) -> None:
self.close()

def _build_network_profile(self):
def _load_account(self) -> dict:
account = account_service.get_account(self.access_token) if self.access_token else {}
account = account if isinstance(account, dict) else {}
return account if isinstance(account, dict) else {}

def _build_network_profile(self):
account = self.account
global_fp = config.data.get("chatgpt_fingerprint")
global_fp = global_fp if isinstance(global_fp, dict) else {}
return build_chatgpt_web_profile(account, global_fp)
Expand Down
2 changes: 1 addition & 1 deletion services/providers/gemini/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ def validate_remote_info(access_token: str, account: dict[str, Any] | None = Non
if access_token:
source.setdefault("access_token", access_token)
cookie_header_value = account_cookie_header(source)
with GeminiWebClient(cookie_header_value, source.get("user_agent")) as client:
with GeminiWebClient(cookie_header_value, source.get("user_agent"), account=source) as client:
client.rotate_psidts()
session_token = client.bootstrap_session_token()
return gemini_session_writeback(source, client.cookie_header, session_token)
Expand Down
26 changes: 22 additions & 4 deletions services/providers/gemini/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,23 @@ def raise_unsupported_image_input() -> None:
raise HTTPException(status_code=400, detail={"error": GEMINI_WEB_IMAGE_UNSUPPORTED_DETAIL})


def contains_image_content(value: object) -> bool:
if isinstance(value, dict):
block_type = str(value.get("type") or "").strip()
if block_type in GEMINI_IMAGE_PART_TYPES:
return True
if any(key in value for key in GEMINI_IMAGE_PAYLOAD_KEYS):
return True
return any(contains_image_content(item) for item in value.values())
if isinstance(value, list):
return any(contains_image_content(item) for item in value)
return False


def raise_unsupported_image_input() -> None:
raise HTTPException(status_code=400, detail={"error": GEMINI_WEB_IMAGE_UNSUPPORTED_DETAIL})


def message_text(content: object) -> str:
if isinstance(content, str):
return content
Expand Down Expand Up @@ -459,11 +476,12 @@ def parse_web_response_text(raw_text: str) -> object:


class GeminiWebClient:
def __init__(self, cookie_header: str, user_agent: str | None = None) -> None:
def __init__(self, cookie_header: str, user_agent: str | None = None, account: dict[str, Any] | None = None) -> None:
self.cookie_header = cookie_header
self.user_agent = user_agent or GEMINI_BROWSER_USER_AGENT
self.session_token = ""
self.session = create_session()
self.account = account if isinstance(account, dict) else None
self.session = create_session(account=self.account)

def __enter__(self) -> "GeminiWebClient":
return self
Expand Down Expand Up @@ -587,7 +605,7 @@ def fetch_authenticated_init_body() -> str:
return ""
account = account_service.get_account(access_token) or {"access_token": access_token, "provider": "gemini"}
cookie_header = account_cookie_header(account)
with GeminiWebClient(cookie_header, account.get("user_agent")) as client:
with GeminiWebClient(cookie_header, account.get("user_agent"), account=account) as client:
init_body = client.fetch_init_body()
persist_gemini_session(account_service, access_token, account, client.cookie_header)
return init_body
Expand All @@ -614,7 +632,7 @@ def chat_completion(body: dict[str, Any], spec: ModelSpec, messages: list[dict[s
if session_token:
payload["session_token"] = session_token
try:
with GeminiWebClient(cookie_header, account.get("user_agent")) as client:
with GeminiWebClient(cookie_header, account.get("user_agent"), account=account) as client:
response_payload = client.generate(payload)
persist_gemini_session(account_service, access_token, account, client.cookie_header, client.session_token)
except GeminiWebError as exc:
Expand Down
8 changes: 6 additions & 2 deletions services/providers/grok/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,11 @@ class GrokConsoleClient:
def __init__(self, access_token: str) -> None:
self.access_token = access_token
self.network_profile = _grok_console_profile()
self.session = create_session(impersonate=self.network_profile.impersonate, verify=self.network_profile.verify)
from services.account_service import account_service

account = account_service.get_account(access_token, provider=GROK_PROVIDER)
account = account if isinstance(account, dict) else None
self.session = create_session(impersonate=self.network_profile.impersonate, verify=self.network_profile.verify, account=account)

def close(self) -> None:
self.session.close()
Expand Down Expand Up @@ -1840,7 +1844,7 @@ def __init__(self, access_token: str, account: dict[str, Any] | None = None) ->
self.account = account if isinstance(account, dict) else None
self.network_profile = _grok_app_chat_profile()
impersonate = _app_chat_impersonate(self.network_profile, self.account)
self.session = create_session(impersonate=impersonate, verify=self.network_profile.verify)
self.session = create_session(impersonate=impersonate, verify=self.network_profile.verify, account=self.account)

def close(self) -> None:
self.session.close()
Expand Down
11 changes: 9 additions & 2 deletions services/proxy_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@


class ProxySettingsStore:
def build_session_kwargs(self, **session_kwargs) -> dict[str, object]:
proxy = config.get_proxy_settings()
def resolve_proxy(self, account: dict | None = None) -> str:
if isinstance(account, dict):
proxy = _clean(account.get("proxy"))
if proxy:
return proxy
return config.get_proxy_settings()

def build_session_kwargs(self, *, account: dict | None = None, **session_kwargs) -> dict[str, object]:
proxy = self.resolve_proxy(account)
if proxy:
session_kwargs["proxy"] = proxy
return session_kwargs
Expand Down
Loading
Loading