Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5082da78ab | |||
| bfb9c15f13 | |||
| 6b4cdec1ec | |||
| 52cef5a7c0 | |||
| 80e7de6183 | |||
| 27cfe1018e |
2
.github/workflows/release_skill.yaml
vendored
2
.github/workflows/release_skill.yaml
vendored
@@ -6,6 +6,8 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
uses: admin/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@main
|
||||
secrets:
|
||||
PYARMOR_REG_B64: ${{ secrets.PYARMOR_REG_B64 }}
|
||||
with:
|
||||
artifact_platform: windows
|
||||
pyarmor_platform: windows.x86_64
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.env
|
||||
25
SKILL.md
25
SKILL.md
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 账号管理
|
||||
description: 多平台多账号管理。管理各平台账号与Chrome Profile的对应关系,供publisher类Skill调用获取账号信息。
|
||||
version: 1.0.3
|
||||
version: 1.0.4
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
@@ -23,22 +23,31 @@ allowed-tools:
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### 添加账号(仅需平台,可选手机号;ID 数据库自增,名称自动生成如「搜狐1号」)
|
||||
### 添加账号(平台用中文名;须填合法中国大陆 11 位手机号;同平台下同手机号不可重复;ID 自增,名称如「搜狐1号」)
|
||||
```bash
|
||||
python3 {baseDir}/scripts/account.py add sohu
|
||||
python3 {baseDir}/scripts/account.py add sohu 189xxxxxxxx
|
||||
python3 {baseDir}/scripts/main.py add 搜狐号 189xxxxxxxx
|
||||
python3 {baseDir}/scripts/main.py add 知乎 13800138000
|
||||
```
|
||||
支持的平台称呼示例:搜狐号、头条号、知乎、微信公众号、Kimi、DeepSeek、豆包、通义千问、文心一言、腾讯元宝(亦支持英文键如 sohu、toutiao)。
|
||||
|
||||
### 仅打开浏览器核对是否已登录(不写数据库)
|
||||
```bash
|
||||
python3 {baseDir}/scripts/account.py open <id>
|
||||
python3 {baseDir}/scripts/main.py open <id>
|
||||
```
|
||||
|
||||
### 登录并自动检测、写入数据库
|
||||
```bash
|
||||
python3 {baseDir}/scripts/account.py login <id>
|
||||
python3 {baseDir}/scripts/main.py login <id>
|
||||
```
|
||||
|
||||
### 列出某平台所有账号
|
||||
### 删除账号(同时删库里的记录与 profile 用户数据目录)
|
||||
```bash
|
||||
python3 {baseDir}/scripts/account.py list <platform>
|
||||
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.
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