Skip to content

Commit 2b991bd

Browse files
v0.3: Account pool, rotation, WebUI, adapter refactor
1 parent 06c11a6 commit 2b991bd

7 files changed

Lines changed: 696 additions & 386 deletions

File tree

.env.example

Lines changed: 26 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,80 +4,45 @@
44
HOST=0.0.0.0
55
PORT=1800
66
API_KEY=sk-web2api-placeholder
7+
ROTATION_STRATEGY=least-recently-used
8+
MAX_ERRORS_BEFORE_DISABLE=3
79

810
# ============================================
9-
# 模型列表(逗号分隔
10-
# 运行 config_tool.py 选择 HAR 文件自动生成 Profile
11+
# Profiles — 模型配置(不含凭证
12+
# 运行 config_tool.py 或通过 WebUI 添加账号
1113
# ============================================
12-
MODEL_NAMES=gemini-3.5-flash,gemini-3.5-flash-adv,gemini-3.1-flash-lite,gemini-3.1-pro,gemini-3.1-pro-ext
14+
PROFILES=gemini-3.5-flash,gemini-3.5-flash-adv,gemini-3.1-pro,gemini-3.1-pro-ext,gemini-3.1-flash-lite,gemini-3.1-flash-lite-adv
1315
DEFAULT_MODEL=gemini-3.5-flash
1416

15-
# ============================================
16-
# Profile: gemini-3.5-flash
17-
# model_family=1 (Flash), thinking_mode=1 (标准)
18-
# ============================================
1917
MODEL_FAMILY_gemini-3.5-flash=1
2018
THINKING_MODE_gemini-3.5-flash=1
21-
F_SID_gemini-3.5-flash=
22-
AT_gemini-3.5-flash=
23-
SN_PARAM_gemini-3.5-flash=
24-
BL_PARAM_gemini-3.5-flash=
25-
HL_gemini-3.5-flash=zh-CN
26-
UUID_gemini-3.5-flash=
27-
HASH_gemini-3.5-flash=
2819

29-
# ============================================
30-
# Profile: gemini-3.5-flash-adv
31-
# model_family=1 (Flash), thinking_mode=2 (进阶思考)
32-
# ============================================
3320
MODEL_FAMILY_gemini-3.5-flash-adv=1
3421
THINKING_MODE_gemini-3.5-flash-adv=2
35-
F_SID_gemini-3.5-flash-adv=
36-
AT_gemini-3.5-flash-adv=
37-
SN_PARAM_gemini-3.5-flash-adv=
38-
BL_PARAM_gemini-3.5-flash-adv=
39-
HL_gemini-3.5-flash-adv=zh-CN
40-
UUID_gemini-3.5-flash-adv=
41-
HASH_gemini-3.5-flash-adv=
42-
43-
# ============================================
44-
# Profile: gemini-3.1-flash-lite
45-
# model_family=6 (Flash Lite), thinking_mode=1 (标准)
46-
# ============================================
47-
MODEL_FAMILY_gemini-3.1-flash-lite=6
48-
THINKING_MODE_gemini-3.1-flash-lite=1
49-
F_SID_gemini-3.1-flash-lite=
50-
AT_gemini-3.1-flash-lite=
51-
SN_PARAM_gemini-3.1-flash-lite=
52-
BL_PARAM_gemini-3.1-flash-lite=
53-
HL_gemini-3.1-flash-lite=zh-CN
54-
UUID_gemini-3.1-flash-lite=
55-
HASH_gemini-3.1-flash-lite=
5622

57-
# ============================================
58-
# Profile: gemini-3.1-pro
59-
# model_family=3 (Pro), thinking_mode=1 (标准思考)
60-
# ============================================
6123
MODEL_FAMILY_gemini-3.1-pro=3
6224
THINKING_MODE_gemini-3.1-pro=1
63-
F_SID_gemini-3.1-pro=
64-
AT_gemini-3.1-pro=
65-
SN_PARAM_gemini-3.1-pro=
66-
BL_PARAM_gemini-3.1-pro=
67-
HL_gemini-3.1-pro=zh-CN
68-
UUID_gemini-3.1-pro=
69-
HASH_gemini-3.1-pro=
7025

71-
# ============================================
72-
# Profile: gemini-3.1-pro-ext
73-
# model_family=3 (Pro), thinking_mode=2 (扩展思考)
74-
# ============================================
7526
MODEL_FAMILY_gemini-3.1-pro-ext=3
7627
THINKING_MODE_gemini-3.1-pro-ext=2
77-
F_SID_gemini-3.1-pro-ext=
78-
AT_gemini-3.1-pro-ext=
79-
SN_PARAM_gemini-3.1-pro-ext=
80-
BL_PARAM_gemini-3.1-pro-ext=
81-
HL_gemini-3.1-pro-ext=zh-CN
82-
UUID_gemini-3.1-pro-ext=
83-
HASH_gemini-3.1-pro-ext=
28+
29+
MODEL_FAMILY_gemini-3.1-flash-lite=6
30+
THINKING_MODE_gemini-3.1-flash-lite=1
31+
32+
MODEL_FAMILY_gemini-3.1-flash-lite-adv=6
33+
THINKING_MODE_gemini-3.1-flash-lite-adv=2
34+
35+
# ============================================
36+
# Accounts — 账号凭证
37+
# 运行 config_tool.py 或通过 WebUI 添加
38+
# 格式: ACCOUNT_<name>_<FIELD>=<value>
39+
# ============================================
40+
# ACCOUNT_myaccount_F_SID=xxx
41+
# ACCOUNT_myaccount_AT=xxx
42+
# ACCOUNT_myaccount_SN_PARAM=xxx
43+
# ACCOUNT_myaccount_BL_PARAM=boq_assistant-bard-web-server_20260525.09_p0
44+
# ACCOUNT_myaccount_HL=zh-CN
45+
# ACCOUNT_myaccount_UUID=
46+
# ACCOUNT_myaccount_HASH=
47+
# ACCOUNT_myaccount_ENABLED=true
48+
# ACCOUNT_myaccount_MODELS=gemini-3.5-flash,gemini-3.1-flash-lite

account_pool.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import json
2+
import time
3+
import os
4+
from dataclasses import dataclass, field
5+
from typing import Optional
6+
7+
8+
@dataclass
9+
class Account:
10+
name: str
11+
f_sid: str = ""
12+
at: str = ""
13+
sn_param: str = ""
14+
bl_param: str = ""
15+
hl: str = "zh-CN"
16+
session_uuid: str = ""
17+
request_hash: str = ""
18+
headers: dict = field(default_factory=dict)
19+
enabled: bool = True
20+
error_count: int = 0
21+
last_used: float = 0.0
22+
bound_models: list = field(default_factory=list)
23+
24+
25+
class AccountPool:
26+
def __init__(self, strategy: str = "least-recently-used", max_errors: int = 3):
27+
self.accounts: list[Account] = []
28+
self.strategy = strategy
29+
self.max_errors = max_errors
30+
self._stats = {"total_requests": 0, "success": 0, "failures": 0, "retries": 0}
31+
32+
def add(self, account: Account):
33+
self.accounts.append(account)
34+
35+
def select(self, model_name: str) -> Optional[Account]:
36+
candidates = [
37+
a for a in self.accounts
38+
if a.enabled and a.error_count < self.max_errors and model_name in a.bound_models
39+
]
40+
if not candidates:
41+
return None
42+
if self.strategy == "least-recently-used":
43+
return min(candidates, key=lambda a: a.last_used)
44+
return candidates[0]
45+
46+
def record_success(self, account: Account):
47+
account.error_count = 0
48+
account.last_used = time.time()
49+
self._stats["success"] += 1
50+
51+
def record_failure(self, account: Account):
52+
account.error_count += 1
53+
self._stats["failures"] += 1
54+
55+
def record_retry(self):
56+
self._stats["retries"] += 1
57+
58+
def record_request(self):
59+
self._stats["total_requests"] += 1
60+
61+
def stats(self) -> dict:
62+
return {
63+
**self._stats,
64+
"accounts_total": len(self.accounts),
65+
"accounts_enabled": sum(1 for a in self.accounts if a.enabled),
66+
"accounts_disabled": sum(1 for a in self.accounts if not a.enabled),
67+
"accounts_exhausted": sum(1 for a in self.accounts if a.error_count >= self.max_errors),
68+
"accounts": [
69+
{
70+
"name": a.name,
71+
"enabled": a.enabled,
72+
"error_count": a.error_count,
73+
"last_used": a.last_used,
74+
"bound_models": a.bound_models,
75+
}
76+
for a in self.accounts
77+
],
78+
}
79+
80+
def load_from_env(self):
81+
prefix = "ACCOUNT_"
82+
accounts_data = {}
83+
for key, val in sorted(os.environ.items()):
84+
if not key.startswith(prefix):
85+
continue
86+
parts = key[len(prefix):].split("_", 1)
87+
if len(parts) != 2:
88+
continue
89+
acct_name, field = parts
90+
if acct_name not in accounts_data:
91+
accounts_data[acct_name] = {"name": acct_name}
92+
if field == "ENABLED":
93+
accounts_data[acct_name]["enabled"] = val.lower() in ("true", "1", "yes")
94+
elif field == "HEADER":
95+
hname, hval = val.split("=", 1) if "=" in val else (val, "")
96+
if "headers" not in accounts_data[acct_name]:
97+
accounts_data[acct_name]["headers"] = {}
98+
accounts_data[acct_name]["headers"][hname] = hval
99+
elif field == "MODELS":
100+
accounts_data[acct_name]["bound_models"] = [m.strip() for m in val.split(",") if m.strip()]
101+
else:
102+
accounts_data[acct_name][field.lower()] = val
103+
104+
for data in accounts_data.values():
105+
self.add(Account(**data))
106+
107+
def get_account(self, name: str) -> Optional[Account]:
108+
for a in self.accounts:
109+
if a.name == name:
110+
return a
111+
return None
112+
113+
def to_env_lines(self) -> list[str]:
114+
lines = []
115+
for a in self.accounts:
116+
lines.append(f"ACCOUNT_{a.name}_F_SID={a.f_sid}")
117+
lines.append(f"ACCOUNT_{a.name}_AT={a.at}")
118+
lines.append(f"ACCOUNT_{a.name}_SN_PARAM={a.sn_param}")
119+
lines.append(f"ACCOUNT_{a.name}_BL_PARAM={a.bl_param}")
120+
lines.append(f"ACCOUNT_{a.name}_HL={a.hl}")
121+
lines.append(f"ACCOUNT_{a.name}_UUID={a.session_uuid}")
122+
lines.append(f"ACCOUNT_{a.name}_HASH={a.request_hash}")
123+
lines.append(f"ACCOUNT_{a.name}_ENABLED={'true' if a.enabled else 'false'}")
124+
lines.append(f"ACCOUNT_{a.name}_MODELS={','.join(a.bound_models)}")
125+
lines.append("")
126+
return lines

0 commit comments

Comments
 (0)