Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5082da78ab | |||
| bfb9c15f13 | |||
| 6b4cdec1ec | |||
| 52cef5a7c0 | |||
| 80e7de6183 | |||
| 27cfe1018e | |||
| ba1c21c0b6 |
2
.github/workflows/release_skill.yaml
vendored
2
.github/workflows/release_skill.yaml
vendored
@@ -6,6 +6,8 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@main
|
uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@main
|
||||||
|
secrets:
|
||||||
|
PYARMOR_REG_B64: ${{ secrets.PYARMOR_REG_B64 }}
|
||||||
with:
|
with:
|
||||||
artifact_platform: windows
|
artifact_platform: windows
|
||||||
pyarmor_platform: windows.x86_64
|
pyarmor_platform: windows.x86_64
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.env
|
||||||
31
SKILL.md
31
SKILL.md
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: 账号管理
|
name: 账号管理
|
||||||
description: 多平台多账号管理。管理各平台账号与Chrome Profile的对应关系,供publisher类Skill调用获取账号信息。
|
description: 多平台多账号管理。管理各平台账号与Chrome Profile的对应关系,供publisher类Skill调用获取账号信息。
|
||||||
version: 1.0.2
|
version: 1.0.4
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
@@ -23,12 +23,31 @@ allowed-tools:
|
|||||||
|
|
||||||
## 执行步骤
|
## 执行步骤
|
||||||
|
|
||||||
### 添加账号(仅需平台,可选手机号;ID 数据库自增,名称自动生成如「搜狐1号」)
|
### 添加账号(平台用中文名;须填合法中国大陆 11 位手机号;同平台下同手机号不可重复;ID 自增,名称如「搜狐1号」)
|
||||||
```bash
|
```bash
|
||||||
python3 {baseDir}/scripts/account.py add sohu
|
python3 {baseDir}/scripts/main.py add 搜狐号 189xxxxxxxx
|
||||||
python3 {baseDir}/scripts/account.py add sohu 189xxxxxxxx
|
python3 {baseDir}/scripts/main.py add 知乎 13800138000
|
||||||
|
```
|
||||||
|
支持的平台称呼示例:搜狐号、头条号、知乎、微信公众号、Kimi、DeepSeek、豆包、通义千问、文心一言、腾讯元宝(亦支持英文键如 sohu、toutiao)。
|
||||||
|
|
||||||
|
### 仅打开浏览器核对是否已登录(不写数据库)
|
||||||
|
```bash
|
||||||
|
python3 {baseDir}/scripts/main.py open <id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 列出某平台所有账号
|
### 登录并自动检测、写入数据库
|
||||||
```bash
|
```bash
|
||||||
python3 {baseDir}/scripts/account.py list <platform>
|
python3 {baseDir}/scripts/main.py login <id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 删除账号(同时删库里的记录与 profile 用户数据目录)
|
||||||
|
```bash
|
||||||
|
python3 {baseDir}/scripts/main.py delete id <id>
|
||||||
|
python3 {baseDir}/scripts/main.py delete platform <平台>
|
||||||
|
python3 {baseDir}/scripts/main.py delete platform <平台> <手机号>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 列出某平台所有账号(platform 可为中文名、all 或 全部)
|
||||||
|
```bash
|
||||||
|
python3 {baseDir}/scripts/main.py list all
|
||||||
|
python3 {baseDir}/scripts/main.py list 搜狐号
|
||||||
Binary file not shown.
@@ -1,7 +1,9 @@
|
|||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
from urllib.parse import urlparse
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
import time
|
||||||
@@ -319,23 +321,95 @@ def _login_poll_interval_seconds():
|
|||||||
return 1.5
|
return 1.5
|
||||||
|
|
||||||
|
|
||||||
|
def _definitely_logged_out_url(page_url: str) -> bool:
|
||||||
|
"""
|
||||||
|
通用:是否「明显仍在登录/认证页」。
|
||||||
|
用路径段判断,避免 mpfe/v4/login 因含 mpfe 被误判为已登录。
|
||||||
|
"""
|
||||||
|
if not page_url or not page_url.strip():
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
p = urlparse(page_url.strip())
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
host = (p.netloc or "").lower()
|
||||||
|
path = (p.path or "").lower()
|
||||||
|
query = (p.query or "").lower()
|
||||||
|
if "passport" in host or "login." in host or "signin." in host:
|
||||||
|
return True
|
||||||
|
seg = [s for s in path.split("/") if s]
|
||||||
|
login_segments = (
|
||||||
|
"login",
|
||||||
|
"signin",
|
||||||
|
"signup",
|
||||||
|
"register",
|
||||||
|
"authorize",
|
||||||
|
"authentication",
|
||||||
|
)
|
||||||
|
if any(s in login_segments for s in seg):
|
||||||
|
return True
|
||||||
|
if re.search(r"/(login|signin)(/|$|\?)", path):
|
||||||
|
return True
|
||||||
|
if "redirect_uri" in query and ("login" in query or "passport" in query):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _sohu_url_logged_in(page_url: str) -> bool:
|
||||||
|
"""
|
||||||
|
搜狐:仅白名单路径视为已进入后台(不用裸 mpfe,避免 /mpfe/v4/login 误判)。
|
||||||
|
"""
|
||||||
|
if _definitely_logged_out_url(page_url):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
path = (urlparse(page_url).path or "").lower()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
if "contentmanagement" in path:
|
||||||
|
return True
|
||||||
|
if "/mpfe/v4/main" in path or path.rstrip("/").endswith("/main"):
|
||||||
|
return True
|
||||||
|
if "/mpfe/v4/publish" in path or "/mpfe/v4/data" in path:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _sohu_page_shows_login_form(page) -> bool:
|
||||||
|
"""搜狐登录页常见密码框;可见则认为未登录(与 URL 交叉验证)。"""
|
||||||
|
try:
|
||||||
|
loc = page.locator('input[type="password"]')
|
||||||
|
n = loc.count()
|
||||||
|
if n == 0:
|
||||||
|
return False
|
||||||
|
return loc.first.is_visible(timeout=800)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _url_looks_logged_in(platform: str, page_url: str) -> bool:
|
def _url_looks_logged_in(platform: str, page_url: str) -> bool:
|
||||||
"""根据当前 URL 判断是否已进入后台(与交互窗口、无头校验共用逻辑)。"""
|
"""是否判定为已登录:搜狐用白名单+负例;其它平台仅用「非明确登录页」宽松策略。"""
|
||||||
u = (page_url or "").lower()
|
if not page_url or not page_url.strip():
|
||||||
if not u:
|
|
||||||
return False
|
return False
|
||||||
if platform == "sohu":
|
if platform == "sohu":
|
||||||
if "passport" in u or "/login" in u:
|
return _sohu_url_logged_in(page_url)
|
||||||
return False
|
if _definitely_logged_out_url(page_url):
|
||||||
# 登录成功后常见:…/mpfe/v4/contentManagement/…
|
|
||||||
if "mpfe" in u or "contentmanagement" in u:
|
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
if "login" in u or "signin" in u or "passport" in u:
|
u = page_url.lower()
|
||||||
|
if "passport" in u or "signin" in u:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _verify_logged_in_page(platform: str, page) -> bool:
|
||||||
|
"""结合 URL 与页面(搜狐加验登录表单)。"""
|
||||||
|
url = page.url or ""
|
||||||
|
if platform == "sohu":
|
||||||
|
if _sohu_page_shows_login_form(page):
|
||||||
|
return False
|
||||||
|
return _sohu_url_logged_in(url)
|
||||||
|
return _url_looks_logged_in(platform, url)
|
||||||
|
|
||||||
|
|
||||||
def _verify_login(profile_dir: str, platform: str, start_url: str, channel: str) -> bool:
|
def _verify_login(profile_dir: str, platform: str, start_url: str, channel: str) -> bool:
|
||||||
try:
|
try:
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
@@ -354,12 +428,11 @@ def _verify_login(profile_dir: str, platform: str, start_url: str, channel: str)
|
|||||||
try:
|
try:
|
||||||
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
||||||
page.goto(start_url, wait_until="domcontentloaded", timeout=45000)
|
page.goto(start_url, wait_until="domcontentloaded", timeout=45000)
|
||||||
# 无头启动后可能仍在跳转,短轮询几次再判定
|
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
if _url_looks_logged_in(platform, page.url):
|
if _verify_logged_in_page(platform, page):
|
||||||
return True
|
return True
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
return _url_looks_logged_in(platform, page.url)
|
return _verify_logged_in_page(platform, page)
|
||||||
finally:
|
finally:
|
||||||
ctx.close()
|
ctx.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -398,20 +471,43 @@ def cmd_login(account_id):
|
|||||||
)
|
)
|
||||||
print(f"轮询间隔约 {poll_sec} 秒;全程最长 {timeout_sec} 秒。")
|
print(f"轮询间隔约 {poll_sec} 秒;全程最长 {timeout_sec} 秒。")
|
||||||
|
|
||||||
# 子进程:轮询 URL,已登录则立即关窗;勿仅用 wait_for_event(close),否则用户不关窗口会卡满整段超时。
|
# 子进程:搜狐轮询 URL+页面;勿用「含 mpfe 即登录」,/mpfe/v4/login 会误判。
|
||||||
runner = r"""import json, sys, time
|
runner = r"""import json, sys, time
|
||||||
|
from urllib.parse import urlparse
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
def looks_sohu(u):
|
def sohu_logged_in(url):
|
||||||
u = (u or "").lower()
|
u = (url or "").strip()
|
||||||
if not u:
|
if not u:
|
||||||
return False
|
return False
|
||||||
if "passport" in u or "/login" in u:
|
try:
|
||||||
|
p = urlparse(u)
|
||||||
|
except Exception:
|
||||||
return False
|
return False
|
||||||
if "mpfe" in u or "contentmanagement" in u:
|
path = (p.path or "").lower()
|
||||||
|
host = (p.netloc or "").lower()
|
||||||
|
if "passport" in host:
|
||||||
|
return False
|
||||||
|
segs = [s for s in path.split("/") if s]
|
||||||
|
if any(s in ("login", "signin", "signup", "register", "authorize") for s in segs):
|
||||||
|
return False
|
||||||
|
if "contentmanagement" in path:
|
||||||
|
return True
|
||||||
|
if "/mpfe/v4/main" in path or path.rstrip("/").endswith("/main"):
|
||||||
|
return True
|
||||||
|
if "/mpfe/v4/publish" in path or "/mpfe/v4/data" in path:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def sohu_not_login_page(page):
|
||||||
|
try:
|
||||||
|
loc = page.locator('input[type="password"]')
|
||||||
|
if loc.count() == 0:
|
||||||
|
return False
|
||||||
|
return loc.first.is_visible(timeout=600)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
with open(sys.argv[1], encoding="utf-8") as f:
|
with open(sys.argv[1], encoding="utf-8") as f:
|
||||||
c = json.load(f)
|
c = json.load(f)
|
||||||
platform = (c.get("platform") or "").lower()
|
platform = (c.get("platform") or "").lower()
|
||||||
@@ -431,7 +527,7 @@ with sync_playwright() as p:
|
|||||||
if platform == "sohu":
|
if platform == "sohu":
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
try:
|
try:
|
||||||
if looks_sohu(page.url):
|
if sohu_logged_in(page.url) and not sohu_not_login_page(page):
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
break
|
break
|
||||||
@@ -498,6 +594,84 @@ with sync_playwright() as p:
|
|||||||
print("⚠️ 未检测到有效登录,状态为未登录。请关闭其他占用该用户目录的浏览器后重试,或延长 JIANGCHANG_LOGIN_TIMEOUT_SECONDS 后再登录。")
|
print("⚠️ 未检测到有效登录,状态为未登录。请关闭其他占用该用户目录的浏览器后重试,或延长 JIANGCHANG_LOGIN_TIMEOUT_SECONDS 后再登录。")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_open(account_id):
|
||||||
|
"""打开该账号的持久化浏览器,仅用于肉眼确认是否已登录;不写数据库。"""
|
||||||
|
target = get_account_by_id(account_id)
|
||||||
|
if not target:
|
||||||
|
print("ERROR:ACCOUNT_NOT_FOUND")
|
||||||
|
return
|
||||||
|
|
||||||
|
channel = resolve_chromium_channel()
|
||||||
|
if not channel:
|
||||||
|
_print_browser_install_hint()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
import playwright # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR:需要 playwright:pip install playwright && playwright install chromium")
|
||||||
|
return
|
||||||
|
|
||||||
|
profile_dir = target["profile_dir"]
|
||||||
|
os.makedirs(profile_dir, exist_ok=True)
|
||||||
|
url = (target.get("url") or "").strip() or PLATFORM_URLS.get(
|
||||||
|
target["platform"], "https://www.google.com"
|
||||||
|
)
|
||||||
|
browser_name = "Google Chrome" if channel == "chrome" else "Microsoft Edge"
|
||||||
|
print(f"正在打开 [{target['name']}] 的 {browser_name}(仅查看,不写入数据库)")
|
||||||
|
print(f"地址:{url}")
|
||||||
|
print("请在本窗口中自行确认登录态。关闭浏览器后命令结束。")
|
||||||
|
print("需要自动检测并写入数据库时,请执行:python account.py login <id>")
|
||||||
|
|
||||||
|
runner_open = r"""import json, sys
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
with open(sys.argv[1], encoding="utf-8") as f:
|
||||||
|
c = json.load(f)
|
||||||
|
with sync_playwright() as p:
|
||||||
|
ctx = p.chromium.launch_persistent_context(
|
||||||
|
user_data_dir=c["profile_dir"],
|
||||||
|
headless=False,
|
||||||
|
channel=c["channel"],
|
||||||
|
no_viewport=True,
|
||||||
|
args=["--start-maximized"],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
||||||
|
page.goto(c["url"], wait_until="domcontentloaded", timeout=60000)
|
||||||
|
ctx.wait_for_event("close", timeout=86400000)
|
||||||
|
except Exception as e:
|
||||||
|
print(e, file=sys.stderr)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
ctx.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
cfg = {"channel": channel, "profile_dir": profile_dir, "url": url}
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="w", suffix=".json", delete=False, encoding="utf-8"
|
||||||
|
) as jf:
|
||||||
|
json.dump(cfg, jf, ensure_ascii=False)
|
||||||
|
cfg_path = jf.name
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="w", suffix=".py", delete=False, encoding="utf-8"
|
||||||
|
) as pf:
|
||||||
|
pf.write(runner_open)
|
||||||
|
py_path = pf.name
|
||||||
|
try:
|
||||||
|
subprocess.run([sys.executable, py_path, cfg_path])
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
os.unlink(cfg_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.unlink(py_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _mark_login_status(account_id, success: bool):
|
def _mark_login_status(account_id, success: bool):
|
||||||
now = _now_unix()
|
now = _now_unix()
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
@@ -521,7 +695,7 @@ def _mark_login_status(account_id, success: bool):
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print(
|
print(
|
||||||
"用法:python account.py add <platform> [phone] | list [platform] | get <id> | login <id>"
|
"用法:python account.py add <platform> [phone] | list [platform] | get <id> | open <id> | login <id>"
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -538,6 +712,8 @@ if __name__ == "__main__":
|
|||||||
cmd_list(sys.argv[2] if len(sys.argv) >= 3 else "all")
|
cmd_list(sys.argv[2] if len(sys.argv) >= 3 else "all")
|
||||||
elif cmd == "get" and len(sys.argv) >= 3:
|
elif cmd == "get" and len(sys.argv) >= 3:
|
||||||
cmd_get(sys.argv[2])
|
cmd_get(sys.argv[2])
|
||||||
|
elif cmd == "open" and len(sys.argv) >= 3:
|
||||||
|
cmd_open(sys.argv[2])
|
||||||
elif cmd == "login" and len(sys.argv) >= 3:
|
elif cmd == "login" and len(sys.argv) >= 3:
|
||||||
cmd_login(sys.argv[2])
|
cmd_login(sys.argv[2])
|
||||||
else:
|
else:
|
||||||
|
|||||||
223
scripts/am_login_child_runner.py
Normal file
223
scripts/am_login_child_runner.py
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
"""Playwright 登录检测子进程:由 main.cmd_login 以独立解释器启动,argv[1] 为 JSON 配置路径。"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
|
||||||
|
def page_location_href(p):
|
||||||
|
# Prefer location.href over page.url for SPA (e.g. Sohu shell path lag).
|
||||||
|
try:
|
||||||
|
href = p.evaluate("() => location.href")
|
||||||
|
if href and str(href).strip():
|
||||||
|
return str(href).strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return (p.url or "").strip()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def host_matches_anchor(netloc, anchor):
|
||||||
|
h = (netloc or "").lower().strip()
|
||||||
|
a = (anchor or "").lower().strip()
|
||||||
|
if not a:
|
||||||
|
return True
|
||||||
|
if not h:
|
||||||
|
return False
|
||||||
|
return h == a or h.endswith("." + a) or a.endswith("." + h)
|
||||||
|
|
||||||
|
|
||||||
|
def loc_first_visible(pg, selectors):
|
||||||
|
for sel in selectors:
|
||||||
|
s = str(sel).strip()
|
||||||
|
if not s:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
loc = pg.locator(s).first
|
||||||
|
if loc.is_visible(timeout=500):
|
||||||
|
return True, s
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def list_pages_on_anchor(ctx, main_page, bundle):
|
||||||
|
anchor = (bundle.get("anchor_host") or "").strip().lower()
|
||||||
|
seen_ids = set()
|
||||||
|
pages = []
|
||||||
|
for pg in list(ctx.pages or []):
|
||||||
|
try:
|
||||||
|
i = id(pg)
|
||||||
|
if i not in seen_ids:
|
||||||
|
seen_ids.add(i)
|
||||||
|
pages.append(pg)
|
||||||
|
except Exception:
|
||||||
|
pages.append(pg)
|
||||||
|
if main_page is not None:
|
||||||
|
try:
|
||||||
|
i = id(main_page)
|
||||||
|
if i not in seen_ids:
|
||||||
|
seen_ids.add(i)
|
||||||
|
pages.append(main_page)
|
||||||
|
except Exception:
|
||||||
|
pages.append(main_page)
|
||||||
|
out = []
|
||||||
|
for pg in pages:
|
||||||
|
try:
|
||||||
|
href = page_location_href(pg)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not href or str(href).lower().startswith("about:"):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
host = urlparse(href).netloc.lower()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if anchor and not host_matches_anchor(host, anchor):
|
||||||
|
continue
|
||||||
|
out.append(pg)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_logged_out_dom(ctx, main_page, bundle):
|
||||||
|
selectors = [
|
||||||
|
str(x).strip()
|
||||||
|
for x in (bundle.get("logged_out_dom_selectors") or [])
|
||||||
|
if x is not None and str(x).strip()
|
||||||
|
]
|
||||||
|
if not selectors:
|
||||||
|
return True, "no_logged_out_dom_selectors_configured"
|
||||||
|
for pg in list_pages_on_anchor(ctx, main_page, bundle):
|
||||||
|
try:
|
||||||
|
href = page_location_href(pg)
|
||||||
|
except Exception:
|
||||||
|
href = ""
|
||||||
|
hit, which = loc_first_visible(pg, selectors)
|
||||||
|
if hit:
|
||||||
|
return True, "logged_out_dom=%r url=%r" % (which, href)
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with open(sys.argv[1], encoding="utf-8") as f:
|
||||||
|
c = json.load(f)
|
||||||
|
bundle = c.get("login_detect_bundle") or {}
|
||||||
|
poll = float(c.get("poll_interval", 1.5))
|
||||||
|
try:
|
||||||
|
dom_grace = float(c.get("dom_grace_sec", 4.0))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
dom_grace = 4.0
|
||||||
|
if dom_grace < 0:
|
||||||
|
dom_grace = 0.0
|
||||||
|
t0 = time.time()
|
||||||
|
deadline = t0 + float(c["timeout_sec"])
|
||||||
|
interactive_ok = False
|
||||||
|
result_path = c.get("result_path") or ""
|
||||||
|
log_path = (c.get("log_file") or "").strip()
|
||||||
|
lg = logging.getLogger("openclaw.skill.account_manager.login_child")
|
||||||
|
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.addHandler(_fh)
|
||||||
|
lg.propagate = False
|
||||||
|
last_eval_detail = ""
|
||||||
|
with sync_playwright() as p:
|
||||||
|
ctx = p.chromium.launch_persistent_context(
|
||||||
|
user_data_dir=c["profile_dir"],
|
||||||
|
headless=False,
|
||||||
|
channel=c["channel"],
|
||||||
|
no_viewport=True,
|
||||||
|
args=["--start-maximized"],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
||||||
|
page.goto(c["url"], wait_until="domcontentloaded", timeout=60000)
|
||||||
|
if dom_grace > 0:
|
||||||
|
time.sleep(dom_grace)
|
||||||
|
if lg.handlers:
|
||||||
|
lg.info(
|
||||||
|
"goto_done initial_pages=%s poll_interval_sec=%s dom_grace_sec=%s",
|
||||||
|
len(ctx.pages or []),
|
||||||
|
poll,
|
||||||
|
dom_grace,
|
||||||
|
)
|
||||||
|
while time.time() < deadline:
|
||||||
|
iter_start = time.time()
|
||||||
|
try:
|
||||||
|
still_logged_out, detail = evaluate_logged_out_dom(ctx, page, bundle)
|
||||||
|
ok = not still_logged_out
|
||||||
|
last_eval_detail = detail
|
||||||
|
if lg.handlers:
|
||||||
|
parts = []
|
||||||
|
for tab in list(ctx.pages or []):
|
||||||
|
try:
|
||||||
|
href = page_location_href(tab)
|
||||||
|
pu = (tab.url or "").strip()
|
||||||
|
parts.append("href=%r playwright_url=%r" % (href, pu))
|
||||||
|
except Exception:
|
||||||
|
parts.append("(tab_read_error)")
|
||||||
|
lg.debug(
|
||||||
|
"poll iter_start_elapsed=%.1fs deadline_in=%.1fs tabs=%s poll_interval_sec=%s logged_out=%s ok=%s detail=%s | %s",
|
||||||
|
iter_start - t0,
|
||||||
|
deadline - iter_start,
|
||||||
|
len(ctx.pages or []),
|
||||||
|
poll,
|
||||||
|
still_logged_out,
|
||||||
|
ok,
|
||||||
|
detail,
|
||||||
|
" ; ".join(parts) if parts else "no_tabs",
|
||||||
|
)
|
||||||
|
if ok:
|
||||||
|
interactive_ok = True
|
||||||
|
if lg.handlers:
|
||||||
|
lg.info("login_detected_ok %s", detail)
|
||||||
|
break
|
||||||
|
except Exception as ex:
|
||||||
|
if lg.handlers:
|
||||||
|
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:
|
||||||
|
lg.warning("login_poll_exhausted detail=%s", last_eval_detail)
|
||||||
|
rem = max(0.0, deadline - time.time())
|
||||||
|
if rem > 0:
|
||||||
|
try:
|
||||||
|
ctx.wait_for_event("close", timeout=rem * 1000)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if lg.handlers:
|
||||||
|
lg.info("login_success_closing_browser_immediately")
|
||||||
|
except Exception as e:
|
||||||
|
if lg.handlers:
|
||||||
|
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)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
ctx.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
34
scripts/am_open_child_runner.py
Normal file
34
scripts/am_open_child_runner.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""Playwright 仅打开浏览器子进程:由 main.cmd_open 启动,argv[1] 为 JSON 配置路径。"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with open(sys.argv[1], encoding="utf-8") as f:
|
||||||
|
c = json.load(f)
|
||||||
|
with sync_playwright() as p:
|
||||||
|
ctx = p.chromium.launch_persistent_context(
|
||||||
|
user_data_dir=c["profile_dir"],
|
||||||
|
headless=False,
|
||||||
|
channel=c["channel"],
|
||||||
|
no_viewport=True,
|
||||||
|
args=["--start-maximized"],
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
||||||
|
page.goto(c["url"], wait_until="domcontentloaded", timeout=60000)
|
||||||
|
ctx.wait_for_event("close", timeout=86400000)
|
||||||
|
except Exception as e:
|
||||||
|
print(e, file=sys.stderr)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
ctx.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1407
scripts/main.py
Normal file
1407
scripts/main.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user