chore: auto release commit (2026-04-06 16:44:58)
All checks were successful
技能自动化发布 / release (push) Successful in 39s
All checks were successful
技能自动化发布 / release (push) Successful in 39s
This commit is contained in:
@@ -8,7 +8,11 @@ import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from db.accounts_repo import get_account_by_id, mark_login_status
|
||||
from util.logging_config import get_skill_logger, get_skill_log_file_path
|
||||
from util.logging_config import (
|
||||
get_skill_logger,
|
||||
get_skill_log_file_path,
|
||||
subprocess_env_with_trace,
|
||||
)
|
||||
from util.platforms import (
|
||||
PLATFORMS,
|
||||
PLATFORM_URLS,
|
||||
@@ -92,6 +96,14 @@ def _login_dom_grace_seconds() -> float:
|
||||
return 4.0
|
||||
|
||||
|
||||
def _wait_login_timeout_seconds() -> int:
|
||||
"""供 wait-login / llm 编排:默认 180 秒,JIANGCHANG_WAIT_LOGIN_TIMEOUT_SECONDS。"""
|
||||
try:
|
||||
return max(30, min(600, int((os.getenv("JIANGCHANG_WAIT_LOGIN_TIMEOUT_SECONDS") or "180").strip())))
|
||||
except ValueError:
|
||||
return 180
|
||||
|
||||
|
||||
def _login_out_bundle_for(platform_key: str) -> dict:
|
||||
"""传给子进程:anchor + 未登录可见的选择器列表。"""
|
||||
spec = PLATFORMS.get(platform_key, {}) if platform_key else {}
|
||||
@@ -111,57 +123,70 @@ def _login_out_bundle_for(platform_key: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def cmd_login(account_id):
|
||||
log = get_skill_logger()
|
||||
log.info("login_command account_id=%r", account_id)
|
||||
target = get_account_by_id(account_id)
|
||||
if not target:
|
||||
log.warning("login_aborted account_not_found account_id=%r", account_id)
|
||||
print("ERROR:ACCOUNT_NOT_FOUND")
|
||||
return
|
||||
def _run_login_browser_session(
|
||||
target: dict,
|
||||
timeout_sec: int,
|
||||
*,
|
||||
verbose_ui: bool = True,
|
||||
) -> tuple[bool, str]:
|
||||
"""
|
||||
启动 Playwright 登录检测子进程;**是否已登录以子进程 DOM 判定为准**。
|
||||
|
||||
成功后尽力 `mark_login_status` 供 list/排序展示,**跨技能编排请勿再依赖该字段**(可能与真实 Cookie 会话脱节)。
|
||||
|
||||
返回 (是否已登录成功, end_reason):end_reason 为 login_child 写入的 reason,失败时可能为
|
||||
timeout / user_closed / subprocess_error。
|
||||
"""
|
||||
log = get_skill_logger()
|
||||
account_id = target.get("id")
|
||||
channel = resolve_chromium_channel()
|
||||
if not channel:
|
||||
log.warning("login_aborted no_chromium_channel")
|
||||
_print_browser_install_hint()
|
||||
return
|
||||
if verbose_ui:
|
||||
_print_browser_install_hint()
|
||||
return False, "no_channel"
|
||||
|
||||
try:
|
||||
import playwright # noqa: F401
|
||||
except ImportError:
|
||||
log.error("login_aborted playwright_missing")
|
||||
print("ERROR:需要 playwright:pip install playwright && playwright install chromium")
|
||||
return
|
||||
if verbose_ui:
|
||||
print("ERROR:需要 playwright:pip install playwright && playwright install chromium")
|
||||
return False, "playwright_missing"
|
||||
|
||||
profile_dir = (target.get("profile_dir") or "").strip()
|
||||
if not profile_dir:
|
||||
log.warning("login_aborted profile_dir_empty account_id=%s", target.get("id"))
|
||||
print("ERROR:PROFILE_DIR_MISSING 库中 profile_dir 为空。")
|
||||
return
|
||||
log.warning("login_aborted profile_dir_empty account_id=%s", account_id)
|
||||
if verbose_ui:
|
||||
print("ERROR:PROFILE_DIR_MISSING 库中 profile_dir 为空。")
|
||||
return False, "profile_missing"
|
||||
|
||||
os.makedirs(profile_dir, exist_ok=True)
|
||||
url = (target.get("url") or "").strip() or PLATFORM_URLS.get(target["platform"], "https://www.google.com")
|
||||
url = (target.get("url") or "").strip() or PLATFORM_URLS.get(
|
||||
target["platform"], "https://www.google.com"
|
||||
)
|
||||
platform = (target.get("platform") or "").strip().lower()
|
||||
timeout_sec = _login_timeout_seconds()
|
||||
poll_sec = _login_poll_interval_seconds()
|
||||
dom_grace_sec = _login_dom_grace_seconds()
|
||||
|
||||
browser_name = "Google Chrome" if channel == "chrome" else "Microsoft Edge"
|
||||
print(f"正在为账号 [{target['name']}] 打开 {browser_name} …")
|
||||
print(f"访问地址:{url}")
|
||||
print(
|
||||
"请在窗口内完成登录。系统根据页面是否仍出现「登录」等未登录控件判断;"
|
||||
"成功后自动关闭窗口并写入状态(无需手动关浏览器)。"
|
||||
)
|
||||
print(
|
||||
f"页面加载后先等待约 {dom_grace_sec:g} 秒再开始检测(减轻跳转误判,可用 JIANGCHANG_LOGIN_DOM_GRACE_SECONDS 调整)。"
|
||||
)
|
||||
print(f"轮询间隔约 {poll_sec} 秒;全程最长 {timeout_sec} 秒。")
|
||||
if verbose_ui:
|
||||
print(f"正在为账号 [{target['name']}] 打开 {browser_name} …")
|
||||
print(f"访问地址:{url}")
|
||||
print(
|
||||
"请在窗口内完成登录。系统根据页面是否仍出现「登录」等未登录控件判断;"
|
||||
"成功后自动关闭窗口并写入状态(无需手动关浏览器)。"
|
||||
)
|
||||
print(
|
||||
f"页面加载后先等待约 {dom_grace_sec:g} 秒再开始检测(减轻跳转误判,可用 JIANGCHANG_LOGIN_DOM_GRACE_SECONDS 调整)。"
|
||||
)
|
||||
print(f"轮询间隔约 {poll_sec} 秒;全程最长 {timeout_sec} 秒。")
|
||||
|
||||
log_file = get_skill_log_file_path()
|
||||
log.info(
|
||||
"login_browser_start account_id=%s platform=%s channel=%s timeout_sec=%s poll_sec=%s "
|
||||
"dom_grace_sec=%s profile_dir=%s start_url=%s log_file=%s",
|
||||
target.get("id"),
|
||||
account_id,
|
||||
platform,
|
||||
channel,
|
||||
timeout_sec,
|
||||
@@ -197,13 +222,16 @@ def cmd_login(account_id):
|
||||
r = subprocess.run(
|
||||
[sys.executable, login_runner_path, cfg_path],
|
||||
timeout=timeout_sec + 180,
|
||||
env=subprocess_env_with_trace(),
|
||||
)
|
||||
proc_rc = r.returncode
|
||||
if proc_rc != 0:
|
||||
print("⚠️ 浏览器进程异常退出,将仅根据已写入的检测结果更新状态")
|
||||
if verbose_ui:
|
||||
print("⚠️ 浏览器进程异常退出,将仅根据已写入的检测结果更新状态")
|
||||
log.warning("login_subprocess_nonzero_return code=%s", proc_rc)
|
||||
except subprocess.TimeoutExpired as ex:
|
||||
print("⚠️ 等待浏览器超时,将仅根据已写入的检测结果更新状态")
|
||||
if verbose_ui:
|
||||
print("⚠️ 等待浏览器超时,将仅根据已写入的检测结果更新状态")
|
||||
log.warning("login_subprocess_timeout err=%s", ex)
|
||||
finally:
|
||||
try:
|
||||
@@ -212,11 +240,16 @@ def cmd_login(account_id):
|
||||
pass
|
||||
|
||||
interactive_ok = False
|
||||
end_reason = "subprocess_error"
|
||||
try:
|
||||
with open(result_path, encoding="utf-8") as rf:
|
||||
interactive_ok = bool(json.load(rf).get("interactive_ok"))
|
||||
data = json.load(rf)
|
||||
interactive_ok = bool(data.get("interactive_ok"))
|
||||
end_reason = str(data.get("end_reason") or "").strip() or (
|
||||
"login_ok" if interactive_ok else "timeout"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
end_reason = "subprocess_error"
|
||||
try:
|
||||
os.unlink(result_path)
|
||||
except OSError:
|
||||
@@ -225,26 +258,100 @@ def cmd_login(account_id):
|
||||
time.sleep(0.5)
|
||||
ok = interactive_ok
|
||||
log.info(
|
||||
"login_finished account_id=%s interactive_ok=%s subprocess_rc=%s marking_db=%s",
|
||||
target.get("id"),
|
||||
"login_finished account_id=%s interactive_ok=%s end_reason=%s subprocess_rc=%s marking_db=%s",
|
||||
account_id,
|
||||
interactive_ok,
|
||||
end_reason,
|
||||
proc_rc,
|
||||
ok,
|
||||
)
|
||||
mark_login_status(target["id"], ok)
|
||||
mark_login_status(account_id, ok)
|
||||
return ok, end_reason if not ok else "login_ok"
|
||||
|
||||
|
||||
def cmd_login(account_id):
|
||||
log = get_skill_logger()
|
||||
log.info("login_command account_id=%r", account_id)
|
||||
target = get_account_by_id(account_id)
|
||||
if not target:
|
||||
log.warning("login_aborted account_not_found account_id=%r", account_id)
|
||||
print("ERROR:ACCOUNT_NOT_FOUND")
|
||||
return
|
||||
|
||||
timeout_sec = _login_timeout_seconds()
|
||||
ok, _end_reason = _run_login_browser_session(target, timeout_sec, verbose_ui=True)
|
||||
log_file = get_skill_log_file_path()
|
||||
if ok:
|
||||
print("✅ 已判定登录成功,状态已写入数据库")
|
||||
else:
|
||||
log.warning(
|
||||
"login_not_detected account_id=%s platform=%s see_log=%s",
|
||||
target.get("id"),
|
||||
platform,
|
||||
(target.get("platform") or "").strip().lower(),
|
||||
log_file,
|
||||
)
|
||||
print("⚠️ 未检测到有效登录,状态为未登录。请关闭其他占用该用户目录的浏览器后重试,或延长 JIANGCHANG_LOGIN_TIMEOUT_SECONDS 后再登录。")
|
||||
print(f"ℹ️ 详细轮询日志见:{log_file}(可将 JIANGCHANG_LOG_LEVEL=DEBUG 打开更细粒度)")
|
||||
|
||||
|
||||
def cmd_wait_login(account_id) -> int:
|
||||
"""
|
||||
跨技能编排:若未登录则打开浏览器并在限时内等待登录。
|
||||
返回 0:已登录(原本已登录或本次登录成功);1:失败(超时 / 用户关浏览器 / 其它)。
|
||||
stdout 首行:OK:WAIT_LOGIN 或 ERROR:WAIT_LOGIN_*。
|
||||
"""
|
||||
log = get_skill_logger()
|
||||
log.info("wait_login_command account_id=%r", account_id)
|
||||
target = get_account_by_id(account_id)
|
||||
if not target:
|
||||
log.warning("wait_login_aborted account_not_found account_id=%r", account_id)
|
||||
print("ERROR:ACCOUNT_NOT_FOUND")
|
||||
return 1
|
||||
# 库内 login_status=1 时跳过开窗(仅性能提示;与真实 Cookie 可能不同步,此时应再执行 wait-login 覆盖)
|
||||
if int(target.get("login_status") or 0) == 1:
|
||||
print("OK:WAIT_LOGIN")
|
||||
return 0
|
||||
|
||||
if not resolve_chromium_channel():
|
||||
_print_browser_install_hint()
|
||||
return 1
|
||||
try:
|
||||
import playwright # noqa: F401
|
||||
except ImportError:
|
||||
print("ERROR:需要 playwright:pip install playwright && playwright install chromium")
|
||||
return 1
|
||||
if not (target.get("profile_dir") or "").strip():
|
||||
print("ERROR:PROFILE_DIR_MISSING 库中 profile_dir 为空。")
|
||||
return 1
|
||||
|
||||
sec = _wait_login_timeout_seconds()
|
||||
print(
|
||||
f"⏳ 账号「{target.get('name', target['id'])}」尚未登录,将打开浏览器;请在 {sec} 秒内完成登录。"
|
||||
"关闭浏览器将视为放弃。"
|
||||
)
|
||||
ok, end_reason = _run_login_browser_session(target, sec, verbose_ui=False)
|
||||
if ok:
|
||||
print("OK:WAIT_LOGIN")
|
||||
return 0
|
||||
if end_reason == "user_closed":
|
||||
print("ERROR:WAIT_LOGIN_ABORTED 已关闭浏览器,未完成登录。")
|
||||
return 1
|
||||
if end_reason == "timeout":
|
||||
print("ERROR:WAIT_LOGIN_TIMEOUT 限时内未完成登录。")
|
||||
return 1
|
||||
if end_reason == "playwright_missing":
|
||||
print("ERROR:需要 playwright:pip install playwright && playwright install chromium")
|
||||
return 1
|
||||
if end_reason == "profile_missing":
|
||||
print("ERROR:PROFILE_DIR_MISSING 库中 profile_dir 为空。")
|
||||
return 1
|
||||
if end_reason == "no_channel":
|
||||
_print_browser_install_hint()
|
||||
return 1
|
||||
print(f"ERROR:WAIT_LOGIN_FAILED end_reason={end_reason}")
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_open(account_id):
|
||||
"""打开该账号的持久化浏览器,仅用于肉眼确认是否已登录;不写数据库。"""
|
||||
get_skill_logger().info("open account_id=%r", account_id)
|
||||
@@ -287,7 +394,10 @@ def cmd_open(account_id):
|
||||
json.dump(cfg, jf, ensure_ascii=False)
|
||||
cfg_path = jf.name
|
||||
try:
|
||||
subprocess.run([sys.executable, open_runner_path, cfg_path])
|
||||
subprocess.run(
|
||||
[sys.executable, open_runner_path, cfg_path],
|
||||
env=subprocess_env_with_trace(),
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(cfg_path)
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
"""Playwright 登录检测子进程:由 browser_service.cmd_login 以独立解释器启动,argv[1] 为 JSON 配置路径。"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
_scripts_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if _scripts_dir not in sys.path:
|
||||
sys.path.insert(0, _scripts_dir)
|
||||
|
||||
from jiangchang_skill_core.unified_logging import attach_unified_file_handler
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
try:
|
||||
from playwright.sync_api import Error as PlaywrightError
|
||||
except ImportError:
|
||||
PlaywrightError = Exception # type: ignore[misc,assignment]
|
||||
|
||||
|
||||
def _is_context_closed_error(ex: BaseException) -> bool:
|
||||
s = str(ex).lower()
|
||||
if "target closed" in s or "browser has been closed" in s or "context was destroyed" in s:
|
||||
return True
|
||||
if isinstance(ex, PlaywrightError) and (
|
||||
"closed" in s or "destroyed" in s
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def page_location_href(p):
|
||||
# Prefer location.href over page.url for SPA (e.g. Sohu shell path lag).
|
||||
@@ -117,21 +138,16 @@ def main():
|
||||
t0 = time.time()
|
||||
deadline = t0 + float(c["timeout_sec"])
|
||||
interactive_ok = False
|
||||
end_reason = "timeout"
|
||||
result_path = c.get("result_path") or ""
|
||||
log_path = (c.get("log_file") or "").strip()
|
||||
lg = logging.getLogger("openclaw.skill.account_manager.login_child")
|
||||
lg = None
|
||||
if log_path:
|
||||
lg.handlers.clear()
|
||||
lg.setLevel(logging.DEBUG)
|
||||
_fh = logging.FileHandler(log_path, encoding="utf-8")
|
||||
_fh.setFormatter(
|
||||
logging.Formatter(
|
||||
"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
|
||||
datefmt="%Y-%m-%dT%H:%M:%S",
|
||||
)
|
||||
lg = attach_unified_file_handler(
|
||||
log_path,
|
||||
skill_slug="account-manager",
|
||||
logger_name="openclaw.skill.account_manager.login_child",
|
||||
)
|
||||
lg.addHandler(_fh)
|
||||
lg.propagate = False
|
||||
last_eval_detail = ""
|
||||
with sync_playwright() as p:
|
||||
ctx = p.chromium.launch_persistent_context(
|
||||
@@ -146,7 +162,7 @@ def main():
|
||||
page.goto(c["url"], wait_until="domcontentloaded", timeout=60000)
|
||||
if dom_grace > 0:
|
||||
time.sleep(dom_grace)
|
||||
if lg.handlers:
|
||||
if lg is not None:
|
||||
lg.info(
|
||||
"goto_done initial_pages=%s poll_interval_sec=%s dom_grace_sec=%s",
|
||||
len(ctx.pages or []),
|
||||
@@ -159,7 +175,7 @@ def main():
|
||||
still_logged_out, detail = evaluate_logged_out_dom(ctx, page, bundle)
|
||||
ok = not still_logged_out
|
||||
last_eval_detail = detail
|
||||
if lg.handlers:
|
||||
if lg is not None:
|
||||
parts = []
|
||||
for tab in list(ctx.pages or []):
|
||||
try:
|
||||
@@ -181,16 +197,22 @@ def main():
|
||||
)
|
||||
if ok:
|
||||
interactive_ok = True
|
||||
if lg.handlers:
|
||||
end_reason = "login_ok"
|
||||
if lg is not None:
|
||||
lg.info("login_detected_ok %s", detail)
|
||||
break
|
||||
except Exception as ex:
|
||||
if lg.handlers:
|
||||
if _is_context_closed_error(ex):
|
||||
end_reason = "user_closed"
|
||||
if lg is not None:
|
||||
lg.warning("login_poll_browser_closed err=%s", ex)
|
||||
break
|
||||
if lg is not None:
|
||||
lg.warning("poll_exception err=%s", ex, exc_info=True)
|
||||
spent = time.time() - iter_start
|
||||
time.sleep(max(0.0, poll - spent))
|
||||
if not interactive_ok:
|
||||
if lg.handlers:
|
||||
if not interactive_ok and end_reason != "user_closed":
|
||||
if lg is not None:
|
||||
lg.warning("login_poll_exhausted detail=%s", last_eval_detail)
|
||||
rem = max(0.0, deadline - time.time())
|
||||
if rem > 0:
|
||||
@@ -199,18 +221,38 @@ def main():
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if lg.handlers:
|
||||
lg.info("login_success_closing_browser_immediately")
|
||||
if lg is not None:
|
||||
lg.info("login_dom_ok_preparing_to_release_profile_dir")
|
||||
if interactive_ok:
|
||||
# 给 Cookie/会话落盘一点时间;随后必须关闭上下文,否则 llm-manager 无法再对同一 profile 启动持久化浏览器
|
||||
print(
|
||||
"INFO:LOGIN_DOM_OK 页面检测已判定登录成功;约 2 秒后关闭本窗口以释放用户目录,供后续大模型网页会话使用。",
|
||||
flush=True,
|
||||
)
|
||||
time.sleep(2.0)
|
||||
except Exception as e:
|
||||
if lg.handlers:
|
||||
lg.exception("login_runner_fatal err=%s", e)
|
||||
print(e, file=sys.stderr)
|
||||
raise
|
||||
if _is_context_closed_error(e):
|
||||
end_reason = "user_closed"
|
||||
interactive_ok = False
|
||||
if lg is not None:
|
||||
lg.warning("login_runner_browser_closed err=%s", e)
|
||||
else:
|
||||
if lg is not None:
|
||||
lg.exception("login_runner_fatal err=%s", e)
|
||||
print(e, file=sys.stderr)
|
||||
raise
|
||||
finally:
|
||||
if result_path:
|
||||
try:
|
||||
with open(result_path, "w", encoding="utf-8") as rf:
|
||||
json.dump({"interactive_ok": interactive_ok}, rf, ensure_ascii=False)
|
||||
json.dump(
|
||||
{
|
||||
"interactive_ok": interactive_ok,
|
||||
"end_reason": end_reason,
|
||||
},
|
||||
rf,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user