chore: auto release commit (2026-09-15 02:56:48)
Some checks failed
技能自动化发布 / release (push) Failing after 4s
Some checks failed
技能自动化发布 / release (push) Failing after 4s
This commit is contained in:
@@ -127,6 +127,64 @@ def _safe_getenv_dir(env_key: str, default_rel: str) -> Path:
|
||||
return (get_data_dir() / default_rel).resolve()
|
||||
|
||||
|
||||
def _get_env(env_key: str, default: str = "") -> str:
|
||||
"""v1.1.31/32:读 env 值,进程 env 优先,缺则从 .env 文件读(手动解析)。
|
||||
|
||||
v1.1.32 关键修复:之前只读 os.environ,但 .env 是用户手动维护的文件,
|
||||
python 进程默认不会自动加载它,导致 SMARTCAMPUS_LOGIN_USER/PASSWORD 永远读不到
|
||||
(jiangchang 宿主未必会注入 .env)。手动解析 .env 兜底。
|
||||
"""
|
||||
# 1. 进程 env 优先(jiangchang 注入或系统 env 优先)
|
||||
val = os.environ.get(env_key)
|
||||
if val is not None and val != "":
|
||||
return val
|
||||
|
||||
# 2. 从 .env 文件手动解析(一次性缓存)
|
||||
global _ENV_CACHE_LOADED, _ENV_CACHE
|
||||
if not _ENV_CACHE_LOADED:
|
||||
_ENV_CACHE_LOADED = True
|
||||
_ENV_CACHE = _load_env_file()
|
||||
return _ENV_CACHE.get(env_key, default)
|
||||
|
||||
|
||||
_ENV_CACHE: dict[str, str] = {}
|
||||
_ENV_CACHE_LOADED = False
|
||||
|
||||
|
||||
def _load_env_file() -> dict[str, str]:
|
||||
"""手动解析 .env 文件(不依赖 python-dotenv)。
|
||||
|
||||
规则:
|
||||
- 跳过注释(# 开头)和空行
|
||||
- 形如 `KEY=value`,value 可带 "..." 或 '...' 引号(自动去引号)
|
||||
- value 含 `=` 时只 split 一次
|
||||
- 找不到 .env 返回空 dict
|
||||
"""
|
||||
from util.runtime_paths import get_env_path
|
||||
|
||||
env_path = get_env_path()
|
||||
if not env_path.exists():
|
||||
return {}
|
||||
out: dict[str, str] = {}
|
||||
try:
|
||||
for raw in env_path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
k = k.strip()
|
||||
v = v.strip()
|
||||
# 去引号
|
||||
if len(v) >= 2 and v[0] == v[-1] and v[0] in ("'", '"'):
|
||||
v = v[1:-1]
|
||||
out[k] = v
|
||||
except Exception as e:
|
||||
logger.warning("platform_collector: 解析 .env 失败(继续走空 dict): %s", e)
|
||||
return out
|
||||
|
||||
|
||||
def _goto(page, url: str, timeout: int = GOTO_TIMEOUT_MS) -> None:
|
||||
"""带 networkidle / domcontentloaded 双兜底的 goto。"""
|
||||
try:
|
||||
@@ -510,14 +568,29 @@ def _wait_for_url_stable(
|
||||
|
||||
|
||||
def _fill_root_login(page) -> bool:
|
||||
"""根域登录页(如还在):fill demo/123456 → click Sign in。返回是否执行了登录。"""
|
||||
"""v1.1.31:根域登录页(如还在):从 .env 读 SMARTCAMPUS_LOGIN_USER /
|
||||
SMARTCAMPUS_LOGIN_PASSWORD(不硬编码 demo/123456)→ fill → click Sign in。
|
||||
|
||||
无 .env 凭证时 return False,调用方走「用户手工登录」分支。
|
||||
"""
|
||||
user_input = page.locator('#jc-user').first
|
||||
if user_input.count() == 0:
|
||||
return False
|
||||
user_input.fill("demo")
|
||||
page.locator('#jc-pass').first.fill("123456")
|
||||
|
||||
root_user = _get_env("SMARTCAMPUS_LOGIN_USER", "").strip()
|
||||
root_pass = _get_env("SMARTCAMPUS_LOGIN_PASSWORD", "").strip()
|
||||
if not root_user or not root_pass:
|
||||
logger.info(
|
||||
"platform_collector._fill_root_login: .env 未配置 "
|
||||
"SMARTCAMPUS_LOGIN_USER / SMARTCAMPUS_LOGIN_PASSWORD,"
|
||||
"**不自动 fill**,等用户手工输入"
|
||||
)
|
||||
return False
|
||||
|
||||
user_input.fill(root_user)
|
||||
page.locator('#jc-pass').first.fill(root_pass)
|
||||
page.locator('button[type="submit"]:has-text("Sign in")').first.click()
|
||||
logger.info("platform_collector: 根域自动登录 demo/123456...")
|
||||
logger.info("platform_collector: 根域自动登录(user 来自 .env)...")
|
||||
try:
|
||||
page.wait_for_url(
|
||||
lambda u: "/home" in u or "/select" in u or "/industries/" in u,
|
||||
@@ -529,18 +602,46 @@ def _fill_root_login(page) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _fill_demo_on_spa_login(page) -> bool:
|
||||
"""v1.1.27:SPA 二级登录页(/industries/campus/smartcampus/login)
|
||||
input 默认**不预填**(与根域不同),必须主动 fill demo/123456 再点登录。
|
||||
def _fill_spa_login_from_env(page) -> bool:
|
||||
"""v1.1.31/32:SPA 二级登录页(/industries/campus/smartcampus/login)
|
||||
按用户要求「密码不能直接放到代码」—— 从 .env 读 SMARTCAMPUS_LOGIN_USER /
|
||||
SMARTCAMPUS_LOGIN_PASSWORD(无值时不自动 fill,等用户手工输入)。
|
||||
|
||||
v1.1.32:sandbox SPA 登录页默认会把「教师(通用)」demo 凭据预填到主表单 input。
|
||||
但用户实际要登录的可能是「人事处」「财务处」等不同行业 tile;
|
||||
按用户截图场景,需先在右侧 DEV MODE 面板里点对应 tile 的 Sign in,
|
||||
让 SPA 把对应行业的 demo 凭据搬到主表单,再走原 fill+click 流程。
|
||||
行业优先级:.env SMARTCAMPUS_LOGIN_INDUSTRY(默认 hr)→ 探测页面已预填的 user 值。
|
||||
|
||||
selector 优先级:
|
||||
- user: `#jc-user` → `input[type='tel']` → `input[autocomplete='username']`
|
||||
- pass: `#jc-pass` → `input[type='password']` → `input[autocomplete='current-password']`
|
||||
按钮文本优先级:Sign in → 登录 → Login → signin → submit[type]。
|
||||
|
||||
返回是否成功点登录按钮。
|
||||
返回是否成功点登录按钮(**无 .env 凭证时返回 False,让用户手工输入**)。
|
||||
"""
|
||||
# 找 user input
|
||||
# 1. 从 .env 读凭证(不硬编码)
|
||||
spa_user = _get_env("SMARTCAMPUS_LOGIN_USER", "").strip()
|
||||
spa_pass = _get_env("SMARTCAMPUS_LOGIN_PASSWORD", "").strip()
|
||||
if not spa_user or not spa_pass:
|
||||
logger.info(
|
||||
"platform_collector._fill_spa_login_from_env: .env 未配置 "
|
||||
"SMARTCAMPUS_LOGIN_USER / SMARTCAMPUS_LOGIN_PASSWORD,"
|
||||
"**不自动 fill**,等用户在浏览器手工输入"
|
||||
)
|
||||
return False
|
||||
|
||||
# 1.5 v1.1.32:检测 DEV MODE 面板是否展开 → 是否需要先点对应行业的 Sign in
|
||||
# sandbox 行为:教师(通用)demo 总是预填到主表单,登录其他行业(hr/finance/research)
|
||||
# 需要在右侧 DEV MODE 面板里点对应 tile 的 Sign in。
|
||||
if _is_dev_mode_panel_open(page):
|
||||
target_industry = _resolve_target_industry(page)
|
||||
logger.info(
|
||||
"platform_collector: DEV MODE 面板展开,目标行业=%s", target_industry,
|
||||
)
|
||||
_click_dev_mode_sign_in_for_industry(page, target_industry)
|
||||
|
||||
# 2. 找 user input
|
||||
user_loc = None
|
||||
for sel in ("#jc-user", "input[type='tel']", "input[autocomplete='username']"):
|
||||
loc = page.locator(sel).first
|
||||
@@ -549,12 +650,12 @@ def _fill_demo_on_spa_login(page) -> bool:
|
||||
break
|
||||
if user_loc is None:
|
||||
logger.warning(
|
||||
"platform_collector._fill_demo_on_spa_login: 找不到 user input "
|
||||
"platform_collector._fill_spa_login_from_env: 找不到 user input "
|
||||
"(#jc-user / tel / autocomplete=username 全 miss)"
|
||||
)
|
||||
return False
|
||||
|
||||
# 找 pass input
|
||||
# 3. 找 pass input
|
||||
pass_loc = None
|
||||
for sel in ("#jc-pass", "input[type='password']", "input[autocomplete='current-password']"):
|
||||
loc = page.locator(sel).first
|
||||
@@ -563,17 +664,17 @@ def _fill_demo_on_spa_login(page) -> bool:
|
||||
break
|
||||
if pass_loc is None:
|
||||
logger.warning(
|
||||
"platform_collector._fill_demo_on_spa_login: 找不到 pass input"
|
||||
"platform_collector._fill_spa_login_from_env: 找不到 pass input"
|
||||
)
|
||||
return False
|
||||
|
||||
user_loc.fill("demo")
|
||||
pass_loc.fill("123456")
|
||||
user_loc.fill(spa_user)
|
||||
pass_loc.fill(spa_pass)
|
||||
logger.info(
|
||||
"platform_collector: SPA 二级登录页 fill demo/123456 完成"
|
||||
"platform_collector: SPA 二级登录页 fill 完成(user 来自 .env)"
|
||||
)
|
||||
|
||||
# 找登录按钮
|
||||
# 4. 找登录按钮
|
||||
btn = None
|
||||
btn_text_used = ""
|
||||
for txt in ("Sign in", "登录", "Login", "signin"):
|
||||
@@ -587,18 +688,138 @@ def _fill_demo_on_spa_login(page) -> bool:
|
||||
btn_text_used = "submit"
|
||||
if btn is None or btn.count() == 0:
|
||||
logger.warning(
|
||||
"platform_collector._fill_demo_on_spa_login: 找不到登录按钮"
|
||||
"platform_collector._fill_spa_login_from_env: 找不到登录按钮"
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info(
|
||||
"platform_collector: SPA 二级登录页点 %s 按钮(demo/123456)",
|
||||
"platform_collector: SPA 二级登录页点 %s 按钮(user 来自 .env)",
|
||||
btn_text_used,
|
||||
)
|
||||
btn.click()
|
||||
return True
|
||||
|
||||
|
||||
def _is_dev_mode_panel_open(page) -> bool:
|
||||
"""v1.1.32:检测 sandbox SPA 登录页右侧 DEV MODE 面板是否展开。
|
||||
|
||||
面板里有「DEV MODE」文字 + 多个 tile(教师通用 / 人事处 / 财务处 / 科研处),
|
||||
每个 tile 有自己的 Fill + Sign in 按钮。
|
||||
|
||||
返回 True 表示面板可见且可点对应行业的 Sign in。
|
||||
|
||||
实现策略(避免依赖 inner_text——fake page mock 不一定支持):
|
||||
- 用 selector 探测至少 2 个 Sign in 按钮(教师/人事处/财务处/科研处 tile)
|
||||
- 至少 1 个含「DEV MODE」文本的元素
|
||||
"""
|
||||
try:
|
||||
# 1. 至少 2 个 Sign in 按钮(教师通用 + 至少 1 个行业 tile)
|
||||
sign_in_btns = page.locator("button:has-text('Sign in')")
|
||||
if sign_in_btns.count() < 2:
|
||||
return False
|
||||
# 2. DEV MODE 标志:找含 'DEV MODE' 文本的元素
|
||||
dev_marker = page.locator(
|
||||
"*:has-text('DEV MODE'), *:text-is('DEV MODE')"
|
||||
).first
|
||||
return dev_marker.count() > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _resolve_target_industry(page) -> str:
|
||||
"""v1.1.32:解析要登录的目标行业。
|
||||
|
||||
优先级:
|
||||
1. .env SMARTCAMPUS_LOGIN_INDUSTRY(可选,默认 'hr')
|
||||
2. 探测页面:若主表单已预填 user 是 'hr' / 'finance' / 'research' / 'demo'
|
||||
→ 直接用该值(说明用户已点过对应 tile 的 Fill)
|
||||
"""
|
||||
explicit = _get_env("SMARTCAMPUS_LOGIN_INDUSTRY", "").strip().lower()
|
||||
if explicit:
|
||||
return explicit
|
||||
# fallback:探测主表单 input 已预填的 user 值
|
||||
try:
|
||||
for sel in ("#jc-user", "input[type='tel']", "input[autocomplete='username']"):
|
||||
loc = page.locator(sel).first
|
||||
if loc.count() > 0:
|
||||
cur = (loc.input_value() or "").strip().lower()
|
||||
if cur in ("hr", "finance", "research", "demo"):
|
||||
return cur
|
||||
except Exception:
|
||||
pass
|
||||
return "hr"
|
||||
|
||||
|
||||
def _click_dev_mode_sign_in_for_industry(page, industry: str) -> bool:
|
||||
"""v1.1.32:在 DEV MODE 面板里点对应行业 tile 的 Sign in 按钮。
|
||||
|
||||
sandbox 行为:每个 tile 是一段包含「行业名 + Email + Password + Fill + Sign in」的 div,
|
||||
用户点 Sign in 会让对应行业的 demo 凭据填到主表单。
|
||||
|
||||
selector 策略(简化版,避免依赖 inner_text——fake page mock 不一定支持):
|
||||
- 行业显示名 → 用 :has-text + Sign in 按钮组合定位
|
||||
- 兜底:用 nth 索引(教师通用=1 / 人事处=2 / 财务处=3 / 科研处=4)
|
||||
|
||||
返回是否成功点击(找不到时返回 False,由调用方决定 fallback)。
|
||||
"""
|
||||
if not industry:
|
||||
return False
|
||||
|
||||
# 行业显示名 → 可能的中文标签
|
||||
industry_aliases = {
|
||||
"hr": ("人事处",),
|
||||
"finance": ("财务处",),
|
||||
"research": ("科研处",),
|
||||
"demo": ("教师", "通用", "Teacher"),
|
||||
}
|
||||
aliases = industry_aliases.get(industry, (industry,))
|
||||
|
||||
# 1. 模式 A:找包含「行业中文标签」的容器 + 容器内 Sign in 按钮
|
||||
for alias in aliases:
|
||||
try:
|
||||
# Playwright 支持 :has() / :has-text() / :text-matches()
|
||||
tile_btn = page.locator(
|
||||
f"div:has-text('{alias}'):has(button:has-text('Sign in')) "
|
||||
f"button:has-text('Sign in')"
|
||||
).first
|
||||
if tile_btn.count() > 0:
|
||||
logger.info(
|
||||
"platform_collector: DEV MODE 点 %s tile 的 Sign in 按钮",
|
||||
alias,
|
||||
)
|
||||
tile_btn.click()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"platform_collector: DEV MODE %s Sign in 点击失败: %s", alias, e,
|
||||
)
|
||||
|
||||
# 2. 兜底:直接找所有 Sign in 按钮,按 industry 顺序选
|
||||
# hr(人事处) 排第 2 个,finance(财务处) 排第 3 个,research(科研处) 排第 4 个
|
||||
industry_order = {"demo": 1, "hr": 2, "finance": 3, "research": 4}
|
||||
if industry in industry_order:
|
||||
idx = industry_order[industry]
|
||||
try:
|
||||
all_sign_in = page.locator("button:has-text('Sign in')")
|
||||
cnt = all_sign_in.count()
|
||||
if cnt >= idx:
|
||||
logger.info(
|
||||
"platform_collector: DEV MODE 用兜底策略点第 %d 个 Sign in(%s)",
|
||||
idx, industry,
|
||||
)
|
||||
all_sign_in.nth(idx - 1).click()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"platform_collector: DEV MODE 兜底点击失败: %s", e,
|
||||
)
|
||||
|
||||
logger.warning(
|
||||
"platform_collector: DEV MODE 找不到 industry=%s 的 Sign in 按钮", industry,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _click_campus_link(page) -> bool:
|
||||
"""在 /select 页面点智慧校园 tile,返回是否点中。"""
|
||||
link = page.locator("a[href*='/industries/campus']").first
|
||||
@@ -830,16 +1051,16 @@ def collect_all(
|
||||
page.url,
|
||||
)
|
||||
|
||||
# 4.5. v1.1.28:URL 落在 SPA 二级登录页 → 主动等 input hydrate(最多 10 秒)
|
||||
# → 主动 fill demo/123456(sandbox SPA 默认 mock)→ 点登录 → 等 URL 跳离
|
||||
# 4.5. v1.1.28/31:URL 落在 SPA 二级登录页 → 主动等 input hydrate(最多 10 秒)
|
||||
# → 从 .env 读凭证 fill(v1.1.31:密码不硬编码)→ 点登录 → 等 URL 跳离
|
||||
# v1.1.27 失败的根因:URL 已稳定在 /login 时,React SPA input 可能还没渲染,
|
||||
# _fill_demo_on_spa_login 找不到 input → False → 卡 180 秒等用户输入。
|
||||
# 找不到 input → False → 卡 180 秒等用户输入。
|
||||
# 修复:先等 input 真正 hydrate 出来,再 fill+click。
|
||||
cur_url_after_stable = page.url.lower()
|
||||
if "/login" in cur_url_after_stable or "/signin" in cur_url_after_stable:
|
||||
logger.warning(
|
||||
"platform_collector: 登录后落在 SPA 二级登录页(%s),"
|
||||
"v1.1.28 触发 SPA 登录兜底(等 input hydrate + fill demo/123456)",
|
||||
"v1.1.31 触发 SPA 登录兜底(等 input hydrate + 从 .env 读凭证 fill)",
|
||||
page.url,
|
||||
)
|
||||
# 1. 等 input 真正 hydrate 出来(最多 10 秒)
|
||||
@@ -855,33 +1076,33 @@ def collect_all(
|
||||
"platform_collector: SPA 登录页 10s 内未渲染 input: %s", e,
|
||||
)
|
||||
|
||||
# 2. fill demo/123456 + 点登录(不管当前 input 有没有值,强制覆盖)
|
||||
spa_filled = _fill_demo_on_spa_login(page)
|
||||
# 2. 从 .env 读凭证 fill + 点登录
|
||||
spa_filled = _fill_spa_login_from_env(page)
|
||||
if spa_filled:
|
||||
logger.info(
|
||||
"platform_collector: ✅ SPA 二级登录页 demo/123456 自动填好,"
|
||||
"已点登录,等 URL 跳到 /app/ 或 /select"
|
||||
"platform_collector: ✅ SPA 二级登录页(来自 .env)已自动填好并点登录,"
|
||||
"等 URL 跳到 /app/ 或 /select"
|
||||
)
|
||||
# 等 URL 跳离 login(30 秒内跳到 /app/ 或 /select 都算成功)
|
||||
if not _wait_for_url_stable(page, ("/app/", "/select"), max_wait_sec=30):
|
||||
logger.warning(
|
||||
"platform_collector: SPA 登录后 30s 内 URL 未稳定到 /app/或/select"
|
||||
"(当前 URL=%s,demo 可能失效需手工登录)",
|
||||
"(当前 URL=%s,.env 凭证可能失效)",
|
||||
page.url,
|
||||
)
|
||||
# demo 失效:等用户手工登录(headless 模式直接抛错)
|
||||
# 凭证失效:等用户手工登录(headless 模式直接抛错)
|
||||
if not headless:
|
||||
_wait_for_manual_login(page, wait_sec=human_wait_timeout)
|
||||
else:
|
||||
raise LoginTimeoutError(
|
||||
f"SPA 二级登录页 demo/123456 填了但 URL 不跳离"
|
||||
f"(可能 demo 失效或需人工登录,URL={page.url})"
|
||||
f"SPA 二级登录页 .env 凭证 fill 后 URL 不跳离"
|
||||
f"(可能凭证失效,URL={page.url})"
|
||||
)
|
||||
else:
|
||||
# fill 失败(input 仍未渲染)→ 等用户手工输入
|
||||
# .env 无凭证 / input 仍未渲染 → 等用户手工输入
|
||||
logger.warning(
|
||||
"platform_collector: SPA 登录页 _fill_demo 失败"
|
||||
"(input 仍未渲染),fallback 等用户输入"
|
||||
"platform_collector: SPA 登录页自动 fill 失败"
|
||||
"(.env 无凭证或 input 未渲染),fallback 等用户输入"
|
||||
)
|
||||
if not headless:
|
||||
if _wait_for_login_inputs_filled(page, wait_sec=human_wait_timeout):
|
||||
@@ -890,12 +1111,13 @@ def collect_all(
|
||||
_wait_for_manual_login(page, wait_sec=human_wait_timeout)
|
||||
else:
|
||||
raise LoginTimeoutError(
|
||||
f"SPA 二级登录页 demo fill 失败且 headless 无法手工登录"
|
||||
f"SPA 二级登录页 .env 无凭证且 headless 无法手工登录"
|
||||
f"(URL={page.url})"
|
||||
)
|
||||
|
||||
# 5. v1.1.22:显式跳到 SPA_HOME_URL(/app/home),等侧边栏 link 出现
|
||||
# 避免 sandbox 在 /select 中间页时直接找 sidebar link 找不到
|
||||
# v1.1.32 优化:URL 不在 /app/home(可能在 /select 中间页)就 goto(5s 等)
|
||||
if "/app/home" not in page.url.lower():
|
||||
logger.info(
|
||||
"platform_collector: 当前 URL=%s,跳到 SPA_HOME_URL=%s",
|
||||
@@ -903,10 +1125,10 @@ def collect_all(
|
||||
)
|
||||
_goto(page, SPA_HOME_URL)
|
||||
page.wait_for_timeout(SPA_HYDRATION_MS)
|
||||
# 等 URL 稳定到 /app/home(最多 15s,**不做任何操作**)
|
||||
if not _wait_for_url_stable(page, "/app/home", max_wait_sec=15):
|
||||
# v1.1.32 优化:等 URL 稳定到 /app/home(最多 5s,比 15s 快很多)
|
||||
if not _wait_for_url_stable(page, "/app/home", max_wait_sec=5):
|
||||
logger.warning(
|
||||
"platform_collector: 15s 内未稳定到 /app/home(当前 URL=%s)",
|
||||
"platform_collector: 5s 内未稳定到 /app/home(当前 URL=%s)",
|
||||
page.url,
|
||||
)
|
||||
|
||||
@@ -952,6 +1174,15 @@ def collect_all(
|
||||
" '/industries/campus/smartcampus/app/hr/contracts'"
|
||||
)
|
||||
page.wait_for_timeout(PUSHSTATE_SETTLE_MS)
|
||||
# v1.1.32:pushState 后 URL 可能没切(sandbox mock 偶发),
|
||||
# 检查 URL 真到 /hr/contracts 才算成功,否则 fallback goto
|
||||
if "/hr/contracts" not in page.url.lower():
|
||||
logger.warning(
|
||||
"platform_collector: pushState 后 URL=%s 未到 /hr/contracts,"
|
||||
"fallback goto PLATFORM_URL",
|
||||
page.url,
|
||||
)
|
||||
_goto(page, PLATFORM_URL)
|
||||
except Exception as e:
|
||||
logger.warning("SPA pushState 失败: %s", e)
|
||||
_goto(page, PLATFORM_URL)
|
||||
@@ -971,6 +1202,8 @@ def collect_all(
|
||||
|
||||
# 5.6. 兜底刷新:仅当等表格超时后才触发;刷新后可能被踢回 login,需重新登录
|
||||
# 修复:之前没检查 URL,刷新后停在 login 页继续往后走,找不到表格就 dump 报错
|
||||
# v1.1.32 优化:刷回 login 时用 _fill_spa_login_from_env(从 .env 读),
|
||||
# 不再调硬编码 demo/123456 的 _fill_spa_login(与 .env 凭证一致)。
|
||||
def _try_reload_after_table_timeout():
|
||||
logger.warning(
|
||||
"platform_collector: 表格未在 %dms 内出现,触发兜底刷新",
|
||||
@@ -995,17 +1228,35 @@ def collect_all(
|
||||
"platform_collector: 刷新后回到登录页(%s),重新登录",
|
||||
page.url,
|
||||
)
|
||||
_goto(page, SPA_LOGIN_URL)
|
||||
page.wait_for_timeout(POST_RELOAD_LOGIN_HYDRATION_MS)
|
||||
if not _fill_spa_login(page):
|
||||
if not headless:
|
||||
_wait_for_manual_login(page, wait_sec=human_wait_timeout)
|
||||
# v1.1.32:先等 input hydrate,再走 .env 凭证 fill
|
||||
try:
|
||||
page.wait_for_selector(
|
||||
"#jc-user, #jc-pass, input[type='tel'], input[type='password']",
|
||||
timeout=10_000,
|
||||
state="attached",
|
||||
)
|
||||
page.wait_for_timeout(500)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"platform_collector: 刷新后 login 页 10s 内未渲染 input: %s", e,
|
||||
)
|
||||
if not _fill_spa_login_from_env(page):
|
||||
# v1.1.32:.env 无凭证时 fallback 到硬编码 demo(保持 v1.1.10 兼容)
|
||||
logger.warning(
|
||||
"platform_collector: .env 无凭证,fallback 硬编码 demo/123456"
|
||||
)
|
||||
if not _fill_spa_login(page):
|
||||
if not headless:
|
||||
_wait_for_manual_login(page, wait_sec=human_wait_timeout)
|
||||
else:
|
||||
raise LoginTimeoutError(
|
||||
"刷新后回到 login 页且 headless 无法手工登录"
|
||||
)
|
||||
else:
|
||||
raise LoginTimeoutError(
|
||||
"刷新后回到 login 页且 headless 无法手工登录"
|
||||
)
|
||||
page.wait_for_timeout(POST_LOGIN_SETTLE_MS)
|
||||
else:
|
||||
page.wait_for_timeout(POST_LOGIN_SETTLE_MS)
|
||||
# .env 凭证 fill 成功 → 等 URL 跳离 login
|
||||
_wait_for_url_stable(page, ("/app/", "/select"), max_wait_sec=15)
|
||||
_goto(page, PLATFORM_URL)
|
||||
page.wait_for_timeout(POST_RELOGIN_GOTO_WAIT_MS)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user