Spaces:
Paused
Paused
fix: get_team_member_count 失败兜底 + account_id UUID 校验 + rotate 超员自动清理
Browse files- get_team_member_count 返回 -1 时用本地 active 数兜底,加详细错误日志
- get_chatgpt_account_id 校验 UUID 格式,state.json 存了 user ID 时 fallback 到 .env
- rotate 发现超员时自动清理多余的本地管理账号,优先移除额度最低的
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- src/autoteam/admin_state.py +12 -1
- src/autoteam/manager.py +34 -3
src/autoteam/admin_state.py
CHANGED
|
@@ -109,9 +109,20 @@ def get_admin_session_token():
|
|
| 109 |
return load_admin_state().get("session_token", "")
|
| 110 |
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
def get_chatgpt_account_id():
|
| 113 |
state = load_admin_state()
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
|
| 117 |
def get_admin_password():
|
|
|
|
| 109 |
return load_admin_state().get("session_token", "")
|
| 110 |
|
| 111 |
|
| 112 |
+
def _is_valid_uuid(value: str) -> bool:
|
| 113 |
+
"""检查是否为有效的 UUID 格式"""
|
| 114 |
+
import re
|
| 115 |
+
|
| 116 |
+
return bool(re.match(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", value, re.I))
|
| 117 |
+
|
| 118 |
+
|
| 119 |
def get_chatgpt_account_id():
|
| 120 |
state = load_admin_state()
|
| 121 |
+
state_id = state.get("account_id", "")
|
| 122 |
+
# state.json 里的值必须是 UUID 格式才有效(user-xxx 是 user ID 不是 account ID)
|
| 123 |
+
if state_id and _is_valid_uuid(state_id):
|
| 124 |
+
return state_id
|
| 125 |
+
return os.environ.get("CHATGPT_ACCOUNT_ID", "")
|
| 126 |
|
| 127 |
|
| 128 |
def get_admin_password():
|
src/autoteam/manager.py
CHANGED
|
@@ -1172,12 +1172,39 @@ def cmd_rotate(target_seats=5):
|
|
| 1172 |
if not chatgpt or not chatgpt.browser:
|
| 1173 |
ensure_chatgpt()
|
| 1174 |
api_count = get_team_member_count(chatgpt)
|
| 1175 |
-
|
| 1176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1177 |
vacancies = TARGET - current_count
|
| 1178 |
|
| 1179 |
if vacancies <= 0:
|
| 1180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1181 |
return
|
| 1182 |
|
| 1183 |
logger.info("[4/5] 填补 %d 个空缺 (当前 %d/%d)...", vacancies, current_count, TARGET)
|
|
@@ -1436,9 +1463,13 @@ def cmd_main_codex_sync():
|
|
| 1436 |
def get_team_member_count(chatgpt_api):
|
| 1437 |
"""获取当前 Team 成员数"""
|
| 1438 |
account_id = get_chatgpt_account_id()
|
|
|
|
|
|
|
|
|
|
| 1439 |
path = f"/backend-api/accounts/{account_id}/users"
|
| 1440 |
result = chatgpt_api._api_fetch("GET", path)
|
| 1441 |
if result["status"] != 200:
|
|
|
|
| 1442 |
return -1
|
| 1443 |
data = json.loads(result["body"])
|
| 1444 |
members = data.get("items", data.get("users", data.get("members", [])))
|
|
|
|
| 1172 |
if not chatgpt or not chatgpt.browser:
|
| 1173 |
ensure_chatgpt()
|
| 1174 |
api_count = get_team_member_count(chatgpt)
|
| 1175 |
+
logger.info("[4/5] API 返回成员数: %d(本轮移出: %d)", api_count, removed_count)
|
| 1176 |
+
if api_count <= 0:
|
| 1177 |
+
# API 返回异常,用本地 active 账号数兜底
|
| 1178 |
+
local_active = sum(1 for a in load_accounts() if a["status"] == STATUS_ACTIVE)
|
| 1179 |
+
logger.warning("[4/5] API 成员数异常 (%d),使用本地 active 数: %d", api_count, local_active)
|
| 1180 |
+
current_count = local_active
|
| 1181 |
+
else:
|
| 1182 |
+
# API 有缓存延迟,移出后可能返回旧数据,手动修正
|
| 1183 |
+
current_count = max(0, api_count - removed_count)
|
| 1184 |
vacancies = TARGET - current_count
|
| 1185 |
|
| 1186 |
if vacancies <= 0:
|
| 1187 |
+
excess = current_count - TARGET
|
| 1188 |
+
if excess > 0:
|
| 1189 |
+
logger.info("[4/5] Team 超员 (%d/%d),清理 %d 个多余成员...", current_count, TARGET, excess)
|
| 1190 |
+
# 只移除本地管理的账号,优先移除额度最低的
|
| 1191 |
+
all_accs = load_accounts()
|
| 1192 |
+
local_active = [a for a in all_accs if a["status"] == STATUS_ACTIVE]
|
| 1193 |
+
# 按额度排序,额度低的优先移除
|
| 1194 |
+
local_active.sort(key=lambda a: 100 - (a.get("last_quota") or {}).get("primary_pct", 0))
|
| 1195 |
+
removed = 0
|
| 1196 |
+
for acc in local_active:
|
| 1197 |
+
if removed >= excess:
|
| 1198 |
+
break
|
| 1199 |
+
email = acc["email"]
|
| 1200 |
+
if remove_from_team(chatgpt, email):
|
| 1201 |
+
update_account(email, status=STATUS_STANDBY)
|
| 1202 |
+
logger.info("[4/5] 超员清理: %s → standby", email)
|
| 1203 |
+
removed += 1
|
| 1204 |
+
if removed:
|
| 1205 |
+
logger.info("[4/5] 已清理 %d 个多余成员", removed)
|
| 1206 |
+
else:
|
| 1207 |
+
logger.info("[4/5] Team 已满 (%d/%d)", current_count, TARGET)
|
| 1208 |
return
|
| 1209 |
|
| 1210 |
logger.info("[4/5] 填补 %d 个空缺 (当前 %d/%d)...", vacancies, current_count, TARGET)
|
|
|
|
| 1463 |
def get_team_member_count(chatgpt_api):
|
| 1464 |
"""获取当前 Team 成员数"""
|
| 1465 |
account_id = get_chatgpt_account_id()
|
| 1466 |
+
if not account_id:
|
| 1467 |
+
logger.error("[Team] account_id 为空,无法查询成员数")
|
| 1468 |
+
return -1
|
| 1469 |
path = f"/backend-api/accounts/{account_id}/users"
|
| 1470 |
result = chatgpt_api._api_fetch("GET", path)
|
| 1471 |
if result["status"] != 200:
|
| 1472 |
+
logger.error("[Team] 获取成员列表失败: %d %s", result["status"], result["body"][:200])
|
| 1473 |
return -1
|
| 1474 |
data = json.loads(result["body"])
|
| 1475 |
members = data.get("items", data.get("users", data.get("members", [])))
|