Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52cef5a7c0 | |||
| 80e7de6183 | |||
| 27cfe1018e | |||
| ba1c21c0b6 | |||
| 58dc81449d |
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: 账号管理
|
||||
description: 多平台多账号管理。管理各平台账号与Chrome Profile的对应关系,供publisher类Skill调用获取账号信息。
|
||||
version: 1.0.2
|
||||
version: 1.0.4
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
@@ -23,12 +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/main.py open <id>
|
||||
```
|
||||
|
||||
### 列出某平台所有账号
|
||||
### 登录并自动检测、写入数据库
|
||||
```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,11 +1,14 @@
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
from urllib.parse import urlparse
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
# Windows GBK 编码兼容修复
|
||||
if sys.platform == "win32":
|
||||
@@ -15,6 +18,40 @@ if sys.platform == "win32":
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SKILL_SLUG = "account-manager"
|
||||
|
||||
# SQLite 无独立 DATETIME 类型:时间统一存 INTEGER Unix 秒(UTC),查询/JSON 再转 ISO8601。
|
||||
# 下列 DDL 含注释,会原样写入 sqlite_master,便于 Navicat / DBeaver 等查看建表语句。
|
||||
ACCOUNTS_TABLE_SQL = """
|
||||
/* 多平台账号表:与本地 Chromium/Edge 用户数据目录(profile)绑定,供发布类技能读取登录态 */
|
||||
CREATE TABLE accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, -- 账号主键(自增)
|
||||
name TEXT NOT NULL, -- 展示名称,如「搜狐1号」
|
||||
platform TEXT NOT NULL, -- 平台标识,与内置 PLATFORM_URLS 键一致,如 sohu
|
||||
phone TEXT, -- 可选绑定手机号
|
||||
profile_dir TEXT, -- Playwright 持久化用户数据目录(绝对路径)
|
||||
url TEXT, -- 平台入口或登录页 URL
|
||||
login_status INTEGER NOT NULL DEFAULT 0, -- 是否已登录:0 否 1 是(由脚本校验后写入)
|
||||
last_login_at INTEGER, -- 最近一次登录成功时间,Unix 秒 UTC;未登录过为 NULL
|
||||
extra_json TEXT, -- 扩展字段 JSON
|
||||
created_at INTEGER NOT NULL, -- 记录创建时间,Unix 秒 UTC
|
||||
updated_at INTEGER NOT NULL -- 记录最后更新时间,Unix 秒 UTC
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
def _now_unix() -> int:
|
||||
return int(time.time())
|
||||
|
||||
|
||||
def _unix_to_iso_local(ts: Optional[int]) -> Optional[str]:
|
||||
"""对外 JSON:本地时区 ISO8601 字符串,便于人读。"""
|
||||
if ts is None:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromtimestamp(int(ts)).isoformat(timespec="seconds")
|
||||
except (ValueError, OSError, OverflowError):
|
||||
return None
|
||||
|
||||
|
||||
PLATFORM_URLS = {
|
||||
"sohu": "https://mp.sohu.com",
|
||||
"zhihu": "https://www.zhihu.com",
|
||||
@@ -75,107 +112,13 @@ def get_conn():
|
||||
return sqlite3.connect(get_db_path())
|
||||
|
||||
|
||||
def _ensure_column(cur, table_name, col_name, ddl):
|
||||
cur.execute(f"PRAGMA table_info({table_name})")
|
||||
cols = [row[1] for row in cur.fetchall()]
|
||||
if col_name not in cols:
|
||||
cur.execute(f"ALTER TABLE {table_name} ADD COLUMN {col_name} {ddl}")
|
||||
|
||||
|
||||
def _has_text_primary_id(cur):
|
||||
cur.execute("PRAGMA table_info(accounts)")
|
||||
for row in cur.fetchall():
|
||||
if row[1] == "id":
|
||||
t = (row[2] or "").upper()
|
||||
return "TEXT" in t or "CHAR" in t
|
||||
return False
|
||||
|
||||
|
||||
def _migrate_text_id_to_integer(conn):
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE accounts_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
profile_dir TEXT,
|
||||
url TEXT,
|
||||
login_status INTEGER DEFAULT 0,
|
||||
last_login_at TEXT,
|
||||
extra_json TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"SELECT name, platform, phone, profile_dir, login_status, last_login_at, extra_json, created_at, updated_at FROM accounts ORDER BY rowid"
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
for row in rows:
|
||||
name, platform, phone, profile_dir, login_status, last_login_at, extra_json, created_at, updated_at = row
|
||||
plat = (platform or "").strip().lower()
|
||||
url = PLATFORM_URLS.get(plat, "")
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO accounts_new
|
||||
(name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
name,
|
||||
plat,
|
||||
phone or "",
|
||||
profile_dir or "",
|
||||
url,
|
||||
int(login_status or 0),
|
||||
last_login_at,
|
||||
extra_json,
|
||||
created_at,
|
||||
updated_at,
|
||||
),
|
||||
)
|
||||
cur.execute("DROP TABLE accounts")
|
||||
cur.execute("ALTER TABLE accounts_new RENAME TO accounts")
|
||||
|
||||
|
||||
def init_db():
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'")
|
||||
if not cur.fetchone():
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
phone TEXT,
|
||||
profile_dir TEXT,
|
||||
url TEXT,
|
||||
login_status INTEGER DEFAULT 0,
|
||||
last_login_at TEXT,
|
||||
extra_json TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
else:
|
||||
if _has_text_primary_id(cur):
|
||||
_migrate_text_id_to_integer(conn)
|
||||
cur = conn.cursor()
|
||||
_ensure_column(cur, "accounts", "url", "TEXT")
|
||||
cur.execute(
|
||||
"SELECT id, platform FROM accounts WHERE url IS NULL OR TRIM(COALESCE(url,'')) = ''"
|
||||
)
|
||||
for rid, plat in cur.fetchall():
|
||||
pu = PLATFORM_URLS.get((plat or "").strip().lower(), "")
|
||||
if pu:
|
||||
cur.execute("UPDATE accounts SET url = ? WHERE id = ?", (pu, rid))
|
||||
cur.executescript(ACCOUNTS_TABLE_SQL)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -197,7 +140,11 @@ def get_account_by_id(account_id):
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT id, name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json FROM accounts WHERE id = ?",
|
||||
"""
|
||||
SELECT id, name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json,
|
||||
created_at, updated_at
|
||||
FROM accounts WHERE id = ?
|
||||
""",
|
||||
(aid,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
@@ -211,7 +158,9 @@ def get_account_by_id(account_id):
|
||||
"profile_dir": row[4] or get_default_profile_dir(row[0]),
|
||||
"url": row[5] or PLATFORM_URLS.get(row[2], ""),
|
||||
"login_status": int(row[6] or 0),
|
||||
"last_login_at": row[7],
|
||||
"last_login_at": _unix_to_iso_local(row[7]),
|
||||
"created_at": _unix_to_iso_local(row[9]),
|
||||
"updated_at": _unix_to_iso_local(row[10]),
|
||||
}
|
||||
if row[8]:
|
||||
try:
|
||||
@@ -279,7 +228,7 @@ def cmd_add(platform: str, phone: str = ""):
|
||||
|
||||
phone = (phone or "").strip()
|
||||
url = PLATFORM_URLS[platform]
|
||||
now = datetime.now().isoformat(timespec="seconds")
|
||||
now = _now_unix()
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
@@ -298,7 +247,7 @@ def cmd_add(platform: str, phone: str = ""):
|
||||
profile_dir = get_default_profile_dir(new_id)
|
||||
cur.execute(
|
||||
"UPDATE accounts SET profile_dir = ?, updated_at = ? WHERE id = ?",
|
||||
(profile_dir, now, new_id),
|
||||
(profile_dir, _now_unix(), new_id),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
@@ -363,6 +312,104 @@ def _login_timeout_seconds():
|
||||
return 300
|
||||
|
||||
|
||||
def _login_poll_interval_seconds():
|
||||
"""交互窗口内轮询地址栏的间隔,默认 1.5s,便于登录成功后尽快收尾。"""
|
||||
try:
|
||||
v = float((os.getenv("JIANGCHANG_LOGIN_POLL_INTERVAL_SECONDS") or "1.5").strip())
|
||||
return max(0.5, min(v, 10.0))
|
||||
except ValueError:
|
||||
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:
|
||||
"""是否判定为已登录:搜狐用白名单+负例;其它平台仅用「非明确登录页」宽松策略。"""
|
||||
if not page_url or not page_url.strip():
|
||||
return False
|
||||
if platform == "sohu":
|
||||
return _sohu_url_logged_in(page_url)
|
||||
if _definitely_logged_out_url(page_url):
|
||||
return False
|
||||
u = page_url.lower()
|
||||
if "passport" in u or "signin" in u:
|
||||
return False
|
||||
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:
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
@@ -381,15 +428,11 @@ def _verify_login(profile_dir: str, platform: str, start_url: str, channel: str)
|
||||
try:
|
||||
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
||||
page.goto(start_url, wait_until="domcontentloaded", timeout=45000)
|
||||
time.sleep(2)
|
||||
u = (page.url or "").lower()
|
||||
if platform == "sohu":
|
||||
if "passport" in u or "/login" in u:
|
||||
return False
|
||||
return True
|
||||
if "login" in u or "signin" in u or "passport" in u:
|
||||
return False
|
||||
return True
|
||||
for _ in range(20):
|
||||
if _verify_logged_in_page(platform, page):
|
||||
return True
|
||||
time.sleep(1.0)
|
||||
return _verify_logged_in_page(platform, page)
|
||||
finally:
|
||||
ctx.close()
|
||||
except Exception:
|
||||
@@ -418,18 +461,58 @@ def cmd_login(account_id):
|
||||
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()
|
||||
|
||||
browser_name = "Google Chrome" if channel == "chrome" else "Microsoft Edge"
|
||||
print(f"正在为账号 [{target['name']}] 打开 {browser_name} …")
|
||||
print(f"访问地址:{url}")
|
||||
print("请在窗口内完成登录(扫码/输入密码)。到达等待时间后将自动关闭窗口,您也可提前手动关闭。")
|
||||
print(f"最长等待:{timeout_sec} 秒")
|
||||
print(
|
||||
"请在窗口内完成登录。系统会轮询地址栏,进入后台后将自动关闭窗口并写入状态(无需手动关浏览器)。"
|
||||
)
|
||||
print(f"轮询间隔约 {poll_sec} 秒;全程最长 {timeout_sec} 秒。")
|
||||
|
||||
# Playwright 同步 API 宜在独立子进程中跑浏览器,避免线程与事件循环问题
|
||||
runner = r"""import json, sys
|
||||
# 子进程:搜狐轮询 URL+页面;勿用「含 mpfe 即登录」,/mpfe/v4/login 会误判。
|
||||
runner = r"""import json, sys, time
|
||||
from urllib.parse import urlparse
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
def sohu_logged_in(url):
|
||||
u = (url or "").strip()
|
||||
if not u:
|
||||
return False
|
||||
try:
|
||||
p = urlparse(u)
|
||||
except Exception:
|
||||
return False
|
||||
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 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:
|
||||
c = json.load(f)
|
||||
platform = (c.get("platform") or "").lower()
|
||||
poll = float(c.get("poll_interval", 1.5))
|
||||
deadline = time.time() + float(c["timeout_sec"])
|
||||
with sync_playwright() as p:
|
||||
ctx = p.chromium.launch_persistent_context(
|
||||
user_data_dir=c["profile_dir"],
|
||||
@@ -441,23 +524,36 @@ with sync_playwright() as p:
|
||||
try:
|
||||
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
||||
page.goto(c["url"], wait_until="domcontentloaded", timeout=60000)
|
||||
try:
|
||||
ctx.wait_for_event("close", timeout=c["timeout_sec"] * 1000)
|
||||
except Exception:
|
||||
pass
|
||||
if platform == "sohu":
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
if sohu_logged_in(page.url) and not sohu_not_login_page(page):
|
||||
break
|
||||
except Exception:
|
||||
break
|
||||
time.sleep(poll)
|
||||
else:
|
||||
rem = max(0.0, deadline - time.time())
|
||||
try:
|
||||
ctx.wait_for_event("close", timeout=rem * 1000)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(e, file=sys.stderr)
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
ctx.close()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(e, file=sys.stderr)
|
||||
raise
|
||||
"""
|
||||
cfg = {
|
||||
"channel": channel,
|
||||
"profile_dir": profile_dir,
|
||||
"url": url,
|
||||
"timeout_sec": timeout_sec,
|
||||
"platform": platform,
|
||||
"poll_interval": poll_sec,
|
||||
}
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False, encoding="utf-8"
|
||||
@@ -498,8 +594,86 @@ with sync_playwright() as p:
|
||||
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):
|
||||
now = datetime.now().isoformat(timespec="seconds")
|
||||
now = _now_unix()
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
@@ -521,7 +695,7 @@ def _mark_login_status(account_id, success: bool):
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
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)
|
||||
|
||||
@@ -538,6 +712,8 @@ if __name__ == "__main__":
|
||||
cmd_list(sys.argv[2] if len(sys.argv) >= 3 else "all")
|
||||
elif cmd == "get" and len(sys.argv) >= 3:
|
||||
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:
|
||||
cmd_login(sys.argv[2])
|
||||
else:
|
||||
|
||||
1654
scripts/main.py
Normal file
1654
scripts/main.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user