Compare commits

...

6 Commits

Author SHA1 Message Date
5082da78ab chore: auto release commit (2026-04-06 10:16:00)
Some checks failed
技能自动化发布 / release (push) Failing after 13s
2026-04-06 10:16:00 +08:00
bfb9c15f13 ci: use kit @main + pass PYARMOR_REG_B64 for licensed CI obfuscation
Some checks failed
技能自动化发布 / release (push) Failing after 12s
Made-with: Cursor
2026-04-05 09:28:58 +08:00
6b4cdec1ec ci: pin reusable workflow to jiangchang-platform-kit@3ee09bb (avoid stale @main)
Some checks failed
技能自动化发布 / release (push) Failing after 12s
Made-with: Cursor
2026-04-05 09:25:05 +08:00
52cef5a7c0 chore: sync from OpenClaw workspace (2026-04-05 09:17)
Some checks failed
技能自动化发布 / release (push) Failing after 11s
2026-04-05 09:17:20 +08:00
80e7de6183 chore: merge origin/main; resolve conflicts
Some checks failed
技能自动化发布 / release (push) Failing after 11s
2026-04-05 08:53:50 +08:00
27cfe1018e chore: initialize account-manager skill repository 2026-04-05 08:36:41 +08:00
7 changed files with 1686 additions and 8 deletions

View File

@@ -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
View File

@@ -0,0 +1,3 @@
__pycache__/
*.py[cod]
.env

View File

@@ -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 搜狐号

View 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()

View 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

File diff suppressed because it is too large Load Diff