Release v1.0.51: precipitate ensure-web, config empty-value, and login-required standards
All checks were successful
技能自动化发布 / release (push) Successful in 7s

This commit is contained in:
2026-07-18 16:30:06 +08:00
parent 0b9a8b2107
commit 1ff70e26a5
23 changed files with 382 additions and 132 deletions

View File

@@ -39,7 +39,7 @@ real_browser_rpa/
## 核心流程
1. **pick account** — 通过 `account_client.pick_web_account()` 获取 `profile_dir` 与租约
1. **ensure account** — 通过 `account_client.pick_web_account()`(底层 `ensure-web`获取 `profile_dir` 与租约;无账号则自动登记,全忙则 `LEASE_CONFLICT` 不重复建号。登记 ≠ 站点已登录。
2. **启动浏览器**`launch_persistent_context` + `new_page()` + `page.goto(start_url)`
3. **人工验证(启动后)** — 检测滑块/短信,等待用户完成
4. **等待登录** — 检测登录按钮消失 / 登录态 marker 出现
@@ -79,7 +79,8 @@ real_browser_rpa/
**浏览器与启动约束:**
- **先 Profile 再开浏览器**:必须 `pick_web_account` 拿到 `profile_dir`;拿不到则失败退出,禁止落到系统默认用户目录(见模板 `development/RPA.md` §0.2
- **先 Profile 再开浏览器**:必须 `pick_web_account`(底层 ensure-web拿到 `profile_dir`;拿不到则失败退出,禁止落到系统默认用户目录(见模板 `development/RPA.md` §0.2
- **RPA 等待**:主路径用 `interruptible_sleep`,禁止裸 `asyncio.sleep``POLICY-CONTROL-003`
- **有头**:生产 / 联调 `OPENCLAW_BROWSER_HEADLESS=0`;勿把无头当交付常态
- **登录按平台**:是否要求站点登录由本技能约定;「可不登录」≠「可不接 account-manager」
- **不要**在技能内安装 Playwright浏览器与 Python 包由宿主/runtime 提供
@@ -142,7 +143,7 @@ real_browser_rpa/
- **不 import account-manager 内部模块** — 只通过 CLI/subprocess 调用
- **不自动破解验证码** — 滑块/短信只检测 + 等待人工完成
- **日志脱敏** — 不输出完整手机号/账号
- **租约释放** — `pick-web --lease` 后必须在 `finally` 释放
- **租约释放** — `ensure-web` / `pick-web --lease` 后必须在 `finally` 释放
- **失败留痕** — 真实 skill 应在关键失败点截图(示例中已留注释位)
## 常见坑

View File

@@ -1,4 +1,7 @@
"""account-manager CLI 集成(仅 subprocess不 import 兄弟 skill 内部模块)。"""
"""account-manager CLI 集成(仅 subprocess不 import 兄弟 skill 内部模块)。
业务首启默认 ``ensure-web``(有则用、无则 add-web登记 ≠ 站点已登录。
"""
from __future__ import annotations
@@ -7,8 +10,9 @@ import logging
import os
import subprocess
import sys
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
from jiangchang_skill_core import config
from jiangchang_skill_core.runtime_env import get_sibling_skills_root
from util.constants import LEASE_HOLDER, LEASE_TTL_SEC, TARGET_PLATFORM
@@ -19,12 +23,17 @@ logger = logging.getLogger(__name__)
PLACEHOLDER_PLATFORM = TARGET_PLATFORM
ACCOUNT_SETUP_MESSAGE = (
f"找到可用的 {PLACEHOLDER_PLATFORM} 账号,所以还没有打开浏览器。"
f"先在 account-manager 中添加 platform={PLACEHOLDER_PLATFORM}"
"status=active、带 profile_dir 的账号后重新运行。"
f"能取得可用的 {PLACEHOLDER_PLATFORM} 账号ensure-web 失败),所以还没有打开浏览器。"
f"确认已安装 account-manager,并检查 platform={PLACEHOLDER_PLATFORM} 账号状态后重试。"
)
LEASE_BUSY_MESSAGE = "目标平台账号当前被其他任务占用,请等待释放租约后重试。"
LEASE_BUSY_MESSAGE = (
"该平台下已有账号,但均被其他任务占用。请稍后重试或释放租约;不会自动新建账号。"
)
DEFAULT_AUTH_STRATEGY = "qr_code_manual"
DEFAULT_ACCOUNT_LABEL = f"{PLACEHOLDER_PLATFORM} 默认"
DEFAULT_START_URL = "https://www.example.com/"
class AccountManagerError(Exception):
@@ -38,13 +47,65 @@ def mask_login_id(login_id: str) -> str:
return mask_text(login_id)
def format_account_label(account: dict) -> str:
"""人读账号标识:优先 login_id / 备注,最后才用数字 id。"""
for key in ("login_id", "account_label", "label"):
v = account.get(key)
if v is not None and str(v).strip():
return str(v).strip()
for key in ("id", "account_id"):
v = account.get(key)
if v is not None and str(v).strip():
return str(v).strip()
return ""
def _is_placeholder_config(raw: str) -> bool:
s = (raw or "").strip()
if not s:
return True
if s.startswith("#"):
return True
low = s.lower()
if low in ("default-account", "none", "null", "n/a", "-"):
return True
return False
def resolve_account_selectors_from_config() -> Tuple[Optional[str], Optional[str]]:
"""从配置解析 (account_id, login_id)。
- ``DEFAULT_ACCOUNT_ID``:仅纯数字视为账号主键;其它非空非占位当作 login_id
- ``DEFAULT_LOGIN_ID``:手机号/用户名(可覆盖上一项推导的 login_id
- 两者皆空:返回 (None, None) → 调用方走按平台 ensure-web
"""
account_id: Optional[str] = None
login_id: Optional[str] = None
aid_raw = (config.get("DEFAULT_ACCOUNT_ID") or "").strip()
if not _is_placeholder_config(aid_raw):
if aid_raw.isdigit():
account_id = aid_raw
else:
login_id = aid_raw
logger.info("default_account_id_treated_as_login_id value=%s", aid_raw)
lid_raw = (config.get("DEFAULT_LOGIN_ID") or "").strip()
if not _is_placeholder_config(lid_raw):
login_id = lid_raw
if account_id:
return account_id, None
return None, login_id
def _resolve_account_manager_main() -> str:
"""解析 account-manager CLI 入口路径。
优先级ACCOUNT_MANAGER_ROOT → get_sibling_skills_root(路径推断 / JIANGCHANG_SKILLS_ROOT→ 开发机兜底。
优先级ACCOUNT_MANAGER_ROOT → get_sibling_skills_root → 开发机兜底。
末尾 dev 路径仅用于模板/本机调试,复制到真实 skill 后不要依赖个人机器绝对路径。
"""
env_root = (os.getenv("ACCOUNT_MANAGER_ROOT") or "").strip()
env_root = (config.get("ACCOUNT_MANAGER_ROOT") or os.getenv("ACCOUNT_MANAGER_ROOT") or "").strip()
if env_root and os.path.isfile(os.path.join(env_root, "scripts", "main.py")):
return os.path.join(os.path.abspath(env_root), "scripts", "main.py")
@@ -103,44 +164,66 @@ def _validate_pick_payload(payload: dict[str, Any]) -> dict[str, Any]:
code = _normalize_error_code(str(err.get("code") or ""))
message = str(err.get("message") or "")
if code == "LEASE_CONFLICT" or "LEASE_CONFLICT" in code:
raise AccountManagerError("LEASE_CONFLICT", LEASE_BUSY_MESSAGE)
raise AccountManagerError("LEASE_CONFLICT", message or LEASE_BUSY_MESSAGE)
if code == "AMBIGUOUS_ACCOUNT" or "AMBIGUOUS_ACCOUNT" in code:
raise AccountManagerError(
"AMBIGUOUS_ACCOUNT",
message or "匹配到多条账号,请改用数字 id 或更精确的登录标识。",
)
if code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in code:
raise AccountManagerError("NO_ACCOUNT", message or "没有可用账号。")
raise AccountManagerError(code or "PICK_WEB_FAILED", message or "pick-web 失败。")
raise AccountManagerError(code or "ENSURE_WEB_FAILED", message or "ensure-web 失败。")
if not isinstance(payload, dict):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。")
raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
if not payload.get("profile_dir"):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回的账号缺少 profile_dir。")
raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回的账号缺少 profile_dir。")
return payload
def _pick_web_with_lease(platform: str) -> Dict[str, Any]:
proc = _run_argv(
[
"account",
"pick-web",
"--platform",
platform,
"--lease",
"--holder",
LEASE_HOLDER,
"--ttl-sec",
LEASE_TTL_SEC,
]
)
def _ensure_web_with_lease(
platform: str,
*,
login_id: Optional[str] = None,
) -> Dict[str, Any]:
start_url = (config.get("TARGET_BASE_URL") or DEFAULT_START_URL).strip() or DEFAULT_START_URL
lid = (login_id or "").strip() or None
label = f"{platform} {lid}" if lid else DEFAULT_ACCOUNT_LABEL
argv = [
"account",
"ensure-web",
"--platform",
platform,
"--url",
start_url,
"--auth-strategy",
DEFAULT_AUTH_STRATEGY,
"--label",
label,
"--lease",
"--holder",
LEASE_HOLDER,
"--ttl-sec",
LEASE_TTL_SEC,
"--purpose",
"rpa",
]
if lid:
argv.extend(["--login-id", lid])
proc = _run_argv(argv)
out = proc.stdout or ""
if proc.returncode != 0 and not out.strip():
raise AccountManagerError(
"PICK_WEB_FAILED",
(proc.stderr or "").strip() or "pick-web 子进程失败",
"ENSURE_WEB_FAILED",
(proc.stderr or "").strip() or "ensure-web 子进程失败",
)
payload = _parse_last_json(out)
if isinstance(payload, dict) and payload.get("success") is False:
return _validate_pick_payload(payload)
if not isinstance(payload, dict):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。")
return payload
raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
return _validate_pick_payload(payload)
def _pick_by_id(platform: str, account_id: int) -> Dict[str, Any]:
@@ -170,19 +253,32 @@ def _pick_by_id(platform: str, account_id: int) -> Dict[str, Any]:
return data
def pick_web_account(platform: str, account_id: Optional[str] = None) -> dict:
"""获取网页账号profile_dir + lease_token"""
def pick_web_account(
platform: str,
account_id: Optional[str] = None,
login_id: Optional[str] = None,
) -> dict:
"""获取网页账号profile_dir + lease_token
优先级:数字 account_id → login_idensure-web --login-id→ 按平台 ensure-web。
"""
platform_key = (platform or PLACEHOLDER_PLATFORM).strip() or PLACEHOLDER_PLATFORM
if account_id:
try:
aid = int(account_id)
except (TypeError, ValueError):
raise AccountManagerError("ACCOUNT_NOT_FOUND", f"账号 ID 无效:{account_id}")
return _pick_by_id(platform_key, aid)
aid_raw = (account_id or "").strip()
if aid_raw and not _is_placeholder_config(aid_raw):
if not aid_raw.isdigit():
if not (login_id or "").strip():
login_id = aid_raw
aid_raw = ""
else:
return _pick_by_id(platform_key, int(aid_raw))
lid = (login_id or "").strip() or None
if lid and _is_placeholder_config(lid):
lid = None
try:
return _pick_web_with_lease(platform_key)
return _ensure_web_with_lease(platform_key, login_id=lid)
except AccountManagerError as exc:
if exc.code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in exc.code:
raise AccountManagerError("ACCOUNT_SETUP_REQUIRED", ACCOUNT_SETUP_MESSAGE) from exc

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
import logging
import os
import random
@@ -11,6 +10,8 @@ import time
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Tuple
from jiangchang_skill_core.activity import interruptible_sleep
from service.browser_session import close_browser_context, get_start_url, start_browser_session
from service.human_verification import (
HumanVerificationWaitResult,
@@ -102,11 +103,11 @@ def _step_cb(cb: Optional[StepCallback], text: str) -> None:
async def _random_delay() -> None:
lo = int(os.getenv("RPA_STEP_DELAY_MIN_MS") or "900")
hi = int(os.getenv("RPA_STEP_DELAY_MAX_MS") or "2600")
await asyncio.sleep(random.uniform(lo / 1000.0, hi / 1000.0))
await interruptible_sleep(random.uniform(lo / 1000.0, hi / 1000.0))
async def _scroll_wait() -> None:
await asyncio.sleep(random.uniform(1.2, 3.5))
await interruptible_sleep(random.uniform(1.2, 3.5))
def _headless() -> bool:
@@ -156,7 +157,7 @@ async def _open_login_panel_if_needed(page) -> None:
btn = page.locator(LOGGED_OUT_SELECTOR).first
if await _visible(btn):
await btn.click()
await asyncio.sleep(random.uniform(1.0, 3.0))
await interruptible_sleep(random.uniform(1.0, 3.0))
except Exception:
pass
@@ -175,7 +176,7 @@ async def _ensure_logged_in(page, *, wait_sec: int) -> None:
if await _is_logged_in(page):
print("[登录] 检测到登录成功")
return
await asyncio.sleep(2.0)
await interruptible_sleep(2.0)
raise RuntimeError(
f"ERROR:LOGIN_TIMEOUT 浏览器已打开,但未完成登录。"
@@ -253,7 +254,7 @@ async def _wait_search_results(page, timeout_sec: float = 45.0) -> bool:
return True
except Exception:
pass
await asyncio.sleep(0.8)
await interruptible_sleep(0.8)
return False

View File

@@ -6,7 +6,12 @@ import asyncio
import uuid
from typing import Any, Optional
from service.account_client import AccountManagerError, pick_web_account, release_lease
from service.account_client import (
AccountManagerError,
pick_web_account,
release_lease,
resolve_account_selectors_from_config,
)
from service.task_rpa import (
ScrapeRunResult,
format_stop_reason_for_user,
@@ -79,7 +84,12 @@ async def run_keyword_search_task(
account: Optional[dict[str, Any]] = None
try:
account = pick_web_account(platform, account_id)
cfg_account_id, cfg_login_id = resolve_account_selectors_from_config()
account = pick_web_account(
platform,
account_id=account_id or cfg_account_id,
login_id=cfg_login_id,
)
lease_token = account.get("lease_token")
scrape_result: ScrapeRunResult = await run_keyword_search_async(

View File

@@ -50,7 +50,7 @@ simulator_browser_rpa/
| 文件 | 职责 |
|---|---|
| `browser_session.py` | async 系统 Chrome/Edge + launch args**不在此 goto** |
| `account_client.py` | **唯一** account-manager subprocess 封装(`pick_web_account` / `release_lease` |
| `account_client.py` | **唯一** account-manager subprocess 封装(`pick_web_account`→ensure-web / `release_lease` |
| `simulator_playwright.py` | 门户轮询、登录、批量表单、PIN、解析 `batch_id`、截图 |
| `adapter/simulator_rpa.py` | **薄** adapterpick 账号、lease、委托 RPA、`finally release_lease` |
| `adapter/mock.py` | 不启浏览器,供 unit/mock/CI |
@@ -59,7 +59,7 @@ simulator_browser_rpa/
## 网页 RPA 硬规则(与模板 `development/RPA.md` §0.2 一致)
- **先 Profile 再开浏览器**`simulator_rpa` 档必须 `pick_web_account` 拿到 `profile_dir`;拿不到则失败,禁止系统默认用户目录
- **先 Profile 再开浏览器**`simulator_rpa` 档必须 `pick_web_account`ensure-web拿到 `profile_dir`;拿不到则失败,禁止系统默认用户目录
- **有头**:联调 / 交付默认 `OPENCLAW_BROWSER_HEADLESS=0`
- **登录按平台约定**:本示例需要仿真登录;其他技能可为 optional / not_needed但**仍须**走 account-manager

View File

@@ -1,4 +1,7 @@
"""account-manager CLI 集成(仅 subprocess不 import 兄弟 skill 内部模块)。"""
"""account-manager CLI 集成(仅 subprocess不 import 兄弟 skill 内部模块)。
业务首启默认 ``ensure-web``(有则用、无则 add-web登记 ≠ 站点已登录。
"""
from __future__ import annotations
@@ -7,8 +10,9 @@ import logging
import os
import subprocess
import sys
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
from jiangchang_skill_core import config
from jiangchang_skill_core.runtime_env import get_sibling_skills_root
from util.constants import LEASE_HOLDER, LEASE_TTL_SEC, TARGET_PLATFORM
@@ -19,12 +23,17 @@ logger = logging.getLogger(__name__)
PLACEHOLDER_PLATFORM = TARGET_PLATFORM
ACCOUNT_SETUP_MESSAGE = (
f"找到可用的 {PLACEHOLDER_PLATFORM} 账号,所以还没有打开浏览器。"
f"先在 account-manager 中添加 platform={PLACEHOLDER_PLATFORM}"
"status=active、带 profile_dir 的账号后重新运行。"
f"能取得可用的 {PLACEHOLDER_PLATFORM} 账号ensure-web 失败),所以还没有打开浏览器。"
f"确认已安装 account-manager,并检查 platform={PLACEHOLDER_PLATFORM} 账号状态后重试。"
)
LEASE_BUSY_MESSAGE = "目标平台账号当前被其他任务占用,请等待释放租约后重试。"
LEASE_BUSY_MESSAGE = (
"该平台下已有账号,但均被其他任务占用。请稍后重试或释放租约;不会自动新建账号。"
)
DEFAULT_AUTH_STRATEGY = "qr_code_manual"
DEFAULT_ACCOUNT_LABEL = f"{PLACEHOLDER_PLATFORM} 默认"
DEFAULT_START_URL = "https://www.example.com/"
class AccountManagerError(Exception):
@@ -38,12 +47,65 @@ def mask_login_id(login_id: str) -> str:
return mask_text(login_id)
def format_account_label(account: dict) -> str:
"""人读账号标识:优先 login_id / 备注,最后才用数字 id。"""
for key in ("login_id", "account_label", "label"):
v = account.get(key)
if v is not None and str(v).strip():
return str(v).strip()
for key in ("id", "account_id"):
v = account.get(key)
if v is not None and str(v).strip():
return str(v).strip()
return ""
def _is_placeholder_config(raw: str) -> bool:
s = (raw or "").strip()
if not s:
return True
if s.startswith("#"):
return True
low = s.lower()
if low in ("default-account", "none", "null", "n/a", "-"):
return True
return False
def resolve_account_selectors_from_config() -> Tuple[Optional[str], Optional[str]]:
"""从配置解析 (account_id, login_id)。
- ``DEFAULT_ACCOUNT_ID``:仅纯数字视为账号主键;其它非空非占位当作 login_id
- ``DEFAULT_LOGIN_ID``:手机号/用户名(可覆盖上一项推导的 login_id
- 两者皆空:返回 (None, None) → 调用方走按平台 ensure-web
"""
account_id: Optional[str] = None
login_id: Optional[str] = None
aid_raw = (config.get("DEFAULT_ACCOUNT_ID") or "").strip()
if not _is_placeholder_config(aid_raw):
if aid_raw.isdigit():
account_id = aid_raw
else:
login_id = aid_raw
logger.info("default_account_id_treated_as_login_id value=%s", aid_raw)
lid_raw = (config.get("DEFAULT_LOGIN_ID") or "").strip()
if not _is_placeholder_config(lid_raw):
login_id = lid_raw
if account_id:
return account_id, None
return None, login_id
def _resolve_account_manager_main() -> str:
"""解析 account-manager CLI 入口路径。
优先级ACCOUNT_MANAGER_ROOT → get_sibling_skills_root → 开发机兜底。
末尾 dev 路径仅用于模板/本机调试,复制到真实 skill 后不要依赖个人机器绝对路径。
"""
env_root = (os.getenv("ACCOUNT_MANAGER_ROOT") or "").strip()
env_root = (config.get("ACCOUNT_MANAGER_ROOT") or os.getenv("ACCOUNT_MANAGER_ROOT") or "").strip()
if env_root and os.path.isfile(os.path.join(env_root, "scripts", "main.py")):
return os.path.join(os.path.abspath(env_root), "scripts", "main.py")
@@ -53,6 +115,7 @@ def _resolve_account_manager_main() -> str:
if os.path.isfile(candidate):
return candidate
# 开发环境兜底:仅模板/本机调试;生产依赖宿主注入的路径变量,勿硬编码个人目录
dev = r"D:\OpenClaw\client-commons\account-manager\scripts\main.py"
if os.path.isfile(dev):
return dev
@@ -101,44 +164,66 @@ def _validate_pick_payload(payload: dict[str, Any]) -> dict[str, Any]:
code = _normalize_error_code(str(err.get("code") or ""))
message = str(err.get("message") or "")
if code == "LEASE_CONFLICT" or "LEASE_CONFLICT" in code:
raise AccountManagerError("LEASE_CONFLICT", LEASE_BUSY_MESSAGE)
raise AccountManagerError("LEASE_CONFLICT", message or LEASE_BUSY_MESSAGE)
if code == "AMBIGUOUS_ACCOUNT" or "AMBIGUOUS_ACCOUNT" in code:
raise AccountManagerError(
"AMBIGUOUS_ACCOUNT",
message or "匹配到多条账号,请改用数字 id 或更精确的登录标识。",
)
if code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in code:
raise AccountManagerError("NO_ACCOUNT", message or "没有可用账号。")
raise AccountManagerError(code or "PICK_WEB_FAILED", message or "pick-web 失败。")
raise AccountManagerError(code or "ENSURE_WEB_FAILED", message or "ensure-web 失败。")
if not isinstance(payload, dict):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。")
raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
if not payload.get("profile_dir"):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回的账号缺少 profile_dir。")
raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回的账号缺少 profile_dir。")
return payload
def _pick_web_with_lease(platform: str) -> Dict[str, Any]:
proc = _run_argv(
[
"account",
"pick-web",
"--platform",
platform,
"--lease",
"--holder",
LEASE_HOLDER,
"--ttl-sec",
LEASE_TTL_SEC,
]
)
def _ensure_web_with_lease(
platform: str,
*,
login_id: Optional[str] = None,
) -> Dict[str, Any]:
start_url = (config.get("TARGET_BASE_URL") or DEFAULT_START_URL).strip() or DEFAULT_START_URL
lid = (login_id or "").strip() or None
label = f"{platform} {lid}" if lid else DEFAULT_ACCOUNT_LABEL
argv = [
"account",
"ensure-web",
"--platform",
platform,
"--url",
start_url,
"--auth-strategy",
DEFAULT_AUTH_STRATEGY,
"--label",
label,
"--lease",
"--holder",
LEASE_HOLDER,
"--ttl-sec",
LEASE_TTL_SEC,
"--purpose",
"rpa",
]
if lid:
argv.extend(["--login-id", lid])
proc = _run_argv(argv)
out = proc.stdout or ""
if proc.returncode != 0 and not out.strip():
raise AccountManagerError(
"PICK_WEB_FAILED",
(proc.stderr or "").strip() or "pick-web 子进程失败",
"ENSURE_WEB_FAILED",
(proc.stderr or "").strip() or "ensure-web 子进程失败",
)
payload = _parse_last_json(out)
if isinstance(payload, dict) and payload.get("success") is False:
return _validate_pick_payload(payload)
if not isinstance(payload, dict):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。")
return payload
raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
return _validate_pick_payload(payload)
def _pick_by_id(platform: str, account_id: int) -> Dict[str, Any]:
@@ -168,19 +253,32 @@ def _pick_by_id(platform: str, account_id: int) -> Dict[str, Any]:
return data
def pick_web_account(platform: str, account_id: Optional[str] = None) -> dict:
"""获取网页账号profile_dir + lease_token"""
def pick_web_account(
platform: str,
account_id: Optional[str] = None,
login_id: Optional[str] = None,
) -> dict:
"""获取网页账号profile_dir + lease_token
优先级:数字 account_id → login_idensure-web --login-id→ 按平台 ensure-web。
"""
platform_key = (platform or PLACEHOLDER_PLATFORM).strip() or PLACEHOLDER_PLATFORM
if account_id:
try:
aid = int(account_id)
except (TypeError, ValueError):
raise AccountManagerError("ACCOUNT_NOT_FOUND", f"账号 ID 无效:{account_id}")
return _pick_by_id(platform_key, aid)
aid_raw = (account_id or "").strip()
if aid_raw and not _is_placeholder_config(aid_raw):
if not aid_raw.isdigit():
if not (login_id or "").strip():
login_id = aid_raw
aid_raw = ""
else:
return _pick_by_id(platform_key, int(aid_raw))
lid = (login_id or "").strip() or None
if lid and _is_placeholder_config(lid):
lid = None
try:
return _pick_web_with_lease(platform_key)
return _ensure_web_with_lease(platform_key, login_id=lid)
except AccountManagerError as exc:
if exc.code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in exc.code:
raise AccountManagerError("ACCOUNT_SETUP_REQUIRED", ACCOUNT_SETUP_MESSAGE) from exc