feat: 实现设置账号并发数功能(支持 bulk-update 与 fallback 逐个更新)

This commit is contained in:
liumangmang
2026-07-02 09:48:59 +08:00
parent a296525d4e
commit 608ef00920
7 changed files with 570 additions and 5 deletions
+170 -1
View File
@@ -41,6 +41,9 @@ from app.schemas.website import (
CleanupInvalidAccountsItem,
CleanupInvalidAccountsPreviewResponse,
CleanupInvalidAccountsExecuteResponse,
SetConcurrencyRequest,
SetConcurrencyItem,
SetConcurrencyResponse,
)
from app.services.website_client import Sub2ApiWebsiteClient, _extract_id
@@ -1106,6 +1109,172 @@ def execute_cleanup_invalid_accounts(wid: int, db: Session = Depends(get_db), _=
)
@router.post("/api/websites/{wid}/accounts/set-concurrency", response_model=SetConcurrencyResponse)
def set_website_accounts_concurrency(
wid: int,
body: SetConcurrencyRequest,
db: Session = Depends(get_db),
_=Depends(get_current_user),
):
"""批量设置当前网站已导入账号的并发数"""
website = db.query(Website).filter(Website.id == wid).first()
if not website:
raise HTTPException(404, "website not found")
if website.site_type != "sub2api":
raise HTTPException(400, "only sub2api site supports account concurrency settings")
c = _client(website)
try:
remote_accounts = c.list_accounts()
except Exception as e:
return SetConcurrencyResponse(
success=False,
message=f"拉取远端账号列表失败: {e}",
items=[]
)
remote_map = {}
if remote_accounts:
for acc in remote_accounts:
aid = c.extract_id(acc)
if aid:
remote_map[aid] = acc
keys = db.query(UpstreamGeneratedKey).filter(
UpstreamGeneratedKey.imported_website_id == wid,
UpstreamGeneratedKey.imported_account_id.isnot(None)
).all()
candidates = {}
for key in keys:
aid = key.imported_account_id
if aid not in candidates:
candidates[aid] = {
"account_id": aid,
"db_key": key
}
if not candidates:
return SetConcurrencyResponse(
success=True,
message="没有找到 SmartUp 导入的有效账号",
items=[]
)
bulk_candidates = []
skipped_items = []
for aid, cand in candidates.items():
remote_acc = remote_map.get(aid)
if not remote_acc:
skipped_items.append(SetConcurrencyItem(
account_id=aid,
account_name=None,
old_concurrency=None,
target_concurrency=body.concurrency,
status="skipped",
message="账号在远端已被删除或不存在"
))
continue
acc_name = remote_acc.get("name")
old_con = remote_acc.get("concurrency")
try:
int(aid)
bulk_candidates.append({
"account_id": aid,
"account_name": acc_name,
"old_concurrency": old_con
})
except ValueError:
skipped_items.append(SetConcurrencyItem(
account_id=aid,
account_name=acc_name,
old_concurrency=old_con,
target_concurrency=body.concurrency,
status="skipped",
message="账号 ID 无法转换为数字,跳过批量更新"
))
items = []
items.extend(skipped_items)
if not bulk_candidates:
success = all(item.status == "skipped" for item in items)
return SetConcurrencyResponse(
success=success,
message=f"无可执行批量更新的账号。已跳过 {len(skipped_items)}",
items=items
)
bulk_ids = [item["account_id"] for item in bulk_candidates]
bulk_success = False
bulk_error_msg = ""
try:
c.bulk_update_accounts(bulk_ids, {"concurrency": body.concurrency})
bulk_success = True
except Exception as e:
bulk_error_msg = str(e)
logger.warning("bulk_update_accounts failed, falling back to individual updates: %s", e)
if bulk_success:
for item in bulk_candidates:
items.append(SetConcurrencyItem(
account_id=item["account_id"],
account_name=item["account_name"],
old_concurrency=item["old_concurrency"],
target_concurrency=body.concurrency,
status="success",
message="成功"
))
else:
for item in bulk_candidates:
aid = item["account_id"]
try:
c.update_account(aid, {"concurrency": body.concurrency})
items.append(SetConcurrencyItem(
account_id=aid,
account_name=item["account_name"],
old_concurrency=item["old_concurrency"],
target_concurrency=body.concurrency,
status="success",
message="成功 (逐个更新)"
))
except Exception as e:
items.append(SetConcurrencyItem(
account_id=aid,
account_name=item["account_name"],
old_concurrency=item["old_concurrency"],
target_concurrency=body.concurrency,
status="failed",
message=f"更新失败: {e}"
))
success_count = sum(1 for item in items if item.status == "success")
failed_count = sum(1 for item in items if item.status == "failed")
skip_count = sum(1 for item in items if item.status == "skipped")
success = (failed_count == 0)
msg_parts = []
if success_count:
msg_parts.append(f"成功 {success_count}")
if failed_count:
msg_parts.append(f"失败 {failed_count}")
if skip_count:
msg_parts.append(f"跳过 {skip_count}")
message = "设置账号并发执行完毕:" + "".join(msg_parts)
return SetConcurrencyResponse(
success=success,
message=message,
items=items
)
@router.post("/api/websites/{wid}/groups/organize", response_model=OrganizeGroupsResponse)
def organize_website_groups(
wid: int,
@@ -1411,7 +1580,7 @@ def organize_website_groups(
},
"group_ids": group_ids,
"rate_multiplier": 1,
"concurrency": 10,
"concurrency": 100,
"priority": priority_val,
"notes": f"Imported by SmartUp from upstream key #{row.id}",
}
+20 -1
View File
@@ -166,7 +166,7 @@ class ImportAccountsRequest(BaseModel):
account_name_prefix: str = "SmartUp"
default_platform: str = "openai"
platform_mode: str = "auto" # "auto" | "manual"
concurrency: int = Field(default=10, ge=1)
concurrency: int = Field(default=100, ge=1, le=1000)
priority: int = Field(default=1, ge=0)
auto_priority_by_rate: bool = True
@@ -268,3 +268,22 @@ class CleanupInvalidAccountsExecuteResponse(BaseModel):
success: bool
message: str
items: list[CleanupInvalidAccountsItem]
class SetConcurrencyRequest(BaseModel):
concurrency: int = Field(default=100, ge=1, le=1000)
class SetConcurrencyItem(BaseModel):
account_id: str
account_name: Optional[str] = None
old_concurrency: Optional[int] = None
target_concurrency: int
status: str # "success" | "skipped" | "failed"
message: str
class SetConcurrencyResponse(BaseModel):
success: bool
message: str
items: list[SetConcurrencyItem]
+15
View File
@@ -297,6 +297,21 @@ class Sub2ApiWebsiteClient:
data = _unwrap_data(resp)
return data if isinstance(data, dict) else {"value": data}
def bulk_update_accounts(self, account_ids: list[str], body: dict[str, Any], endpoint: str = "/accounts/bulk-update") -> dict[str, Any]:
"""批量更新账号。"""
int_ids = []
for aid in account_ids:
try:
int_ids.append(int(aid))
except ValueError:
pass
resp = self._request("POST", endpoint, {
"account_ids": int_ids,
**body
})
data = _unwrap_data(resp)
return data if isinstance(data, dict) else {"value": data}
@staticmethod
def _unwrap_list(value: dict) -> list | None:
"""递归展开嵌套的列表包装:data.items、data.data、items、accounts 等。"""