chore: auto release commit (2026-04-06 12:10:31)
All checks were successful
技能自动化发布 / release (push) Successful in 13s
All checks were successful
技能自动化发布 / release (push) Successful in 13s
This commit is contained in:
1
scripts/util/__init__.py
Normal file
1
scripts/util/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Shared helpers for account-manager (paths, platforms, logging, constants)."""
|
||||
35
scripts/util/constants.py
Normal file
35
scripts/util/constants.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""技能级常量与本地 CLI 调试环境注入(仅 CLI 入口显式调用 _apply_cli_local_dev_env 时生效)。"""
|
||||
import os
|
||||
|
||||
# scripts/util/constants.py -> skill root = parents[2]
|
||||
_BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
BASE_DIR = _BASE
|
||||
SKILL_SLUG = "account-manager"
|
||||
# 与其它 OpenClaw 技能对齐:openclaw.skill.<slug_下划线>
|
||||
LOG_LOGGER_NAME = "openclaw.skill.account_manager"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 本地 CLI 调试:直接运行 python main.py 时,若未设置 JIANGCHANG_*,可自动注入下面默认值。
|
||||
# - 仅当 CLI 里调用了 _apply_cli_local_dev_env() 时生效;被其它脚本 import 不会改 os.environ。
|
||||
# - 不会覆盖宿主/终端已设置的 JIANGCHANG_DATA_ROOT、JIANGCHANG_USER_ID。
|
||||
# - 开启方式(二选一):
|
||||
# 1) 将 _ACCOUNT_MANAGER_CLI_LOCAL_DEV 改为 True;
|
||||
# 2) 或设置环境变量 JIANGCHANG_ACCOUNT_CLI_LOCAL_DEV=1(无需改代码)。
|
||||
# 合并/发版前请关闭开关,避免把个人机器路径带进生产制品。
|
||||
# ---------------------------------------------------------------------------
|
||||
_ACCOUNT_MANAGER_CLI_LOCAL_DEV = True
|
||||
_ACCOUNT_MANAGER_CLI_LOCAL_DATA_ROOT = r"D:\jiangchang-data"
|
||||
_ACCOUNT_MANAGER_CLI_LOCAL_USER_ID = "10032"
|
||||
|
||||
|
||||
def _apply_cli_local_dev_env() -> None:
|
||||
enabled = _ACCOUNT_MANAGER_CLI_LOCAL_DEV
|
||||
if not enabled:
|
||||
v = (os.getenv("JIANGCHANG_ACCOUNT_CLI_LOCAL_DEV") or "").strip().lower()
|
||||
enabled = v in ("1", "true", "yes", "on")
|
||||
if not enabled:
|
||||
return
|
||||
if not (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip():
|
||||
os.environ["JIANGCHANG_DATA_ROOT"] = _ACCOUNT_MANAGER_CLI_LOCAL_DATA_ROOT.strip()
|
||||
if not (os.getenv("JIANGCHANG_USER_ID") or "").strip():
|
||||
os.environ["JIANGCHANG_USER_ID"] = _ACCOUNT_MANAGER_CLI_LOCAL_USER_ID.strip()
|
||||
69
scripts/util/logging_config.py
Normal file
69
scripts/util/logging_config.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""技能级文件日志。"""
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from util.constants import LOG_LOGGER_NAME, SKILL_SLUG
|
||||
from util.runtime_paths import get_skill_logs_dir
|
||||
|
||||
|
||||
def _skill_log_file_path() -> str:
|
||||
"""主日志文件路径;可由 JIANGCHANG_ACCOUNT_MANAGER_LOG_FILE 覆盖为绝对路径。"""
|
||||
override = (os.getenv("JIANGCHANG_ACCOUNT_MANAGER_LOG_FILE") or "").strip()
|
||||
if override:
|
||||
parent = os.path.dirname(os.path.abspath(override))
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
return os.path.abspath(override)
|
||||
return os.path.join(get_skill_logs_dir(), f"{SKILL_SLUG}.log")
|
||||
|
||||
|
||||
def _log_level_from_env() -> int:
|
||||
v = (os.getenv("JIANGCHANG_LOG_LEVEL") or "INFO").strip().upper()
|
||||
return getattr(logging, v, None) or logging.INFO
|
||||
|
||||
|
||||
def get_skill_log_file_path() -> str:
|
||||
"""主日志文件绝对路径(与 get_skill_logger 写入文件一致)。"""
|
||||
return _skill_log_file_path()
|
||||
|
||||
|
||||
def get_skill_logger() -> logging.Logger:
|
||||
"""
|
||||
技能级日志:单文件按日切分(TimedRotatingFileHandler midnight),UTF-8。
|
||||
环境变量:JIANGCHANG_LOG_LEVEL(默认 INFO)、JIANGCHANG_LOG_TO_STDERR=1 时 WARNING+ 同步 stderr、
|
||||
JIANGCHANG_ACCOUNT_MANAGER_LOG_FILE 覆盖日志文件路径。
|
||||
"""
|
||||
log = logging.getLogger(LOG_LOGGER_NAME)
|
||||
if log.handlers:
|
||||
return log
|
||||
log.setLevel(_log_level_from_env())
|
||||
path = _skill_log_file_path()
|
||||
fh = TimedRotatingFileHandler(
|
||||
path,
|
||||
when="midnight",
|
||||
interval=1,
|
||||
backupCount=30,
|
||||
encoding="utf-8",
|
||||
delay=True,
|
||||
)
|
||||
fh.setFormatter(
|
||||
logging.Formatter(
|
||||
"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
|
||||
datefmt="%Y-%m-%dT%H:%M:%S",
|
||||
)
|
||||
)
|
||||
log.addHandler(fh)
|
||||
if (os.getenv("JIANGCHANG_LOG_TO_STDERR") or "").strip().lower() in (
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
):
|
||||
sh = logging.StreamHandler(sys.stderr)
|
||||
sh.setLevel(logging.WARNING)
|
||||
sh.setFormatter(fh.formatter)
|
||||
log.addHandler(sh)
|
||||
log.propagate = False
|
||||
return log
|
||||
139
scripts/util/platforms.py
Normal file
139
scripts/util/platforms.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""平台配置、别名解析、手机号校验。"""
|
||||
import re
|
||||
|
||||
# 平台唯一配置表:只改这里即可。键 = 入库的 platform;label = 帮助/提示里的展示名;
|
||||
# prefix = 默认账号名「{prefix}{序号}号」,省略时等于 label;
|
||||
# aliases = 额外 CLI 称呼(英文键与 label 已自动参与解析,不必重复写)。
|
||||
# 登录检测:仅看页面 DOM。匹配 anchor 的标签页上出现「未登录」选择器则未登录;否则视为已登录。
|
||||
# - _LOGIN_LOGGED_OUT_DOM_GENERIC_SELECTORS:通用「登录」按钮/链接等(可被 login_skip_generic_logged_out_dom 关闭)。
|
||||
# - login_logged_out_selectors:按平台追加;误判时也可用 login_skip_generic_logged_out_dom + 仅平台自选。
|
||||
PLATFORMS = {
|
||||
"sohu": {
|
||||
"url": "https://mp.sohu.com",
|
||||
"label": "搜狐号",
|
||||
"prefix": "搜狐",
|
||||
"aliases": ["搜狐"],
|
||||
},
|
||||
"toutiao": {
|
||||
"url": "https://mp.toutiao.com/",
|
||||
"label": "头条号",
|
||||
"prefix": "头条",
|
||||
"aliases": ["头条"],
|
||||
},
|
||||
"zhihu": {
|
||||
"url": "https://www.zhihu.com",
|
||||
"label": "知乎",
|
||||
"aliases": ["知乎号"],
|
||||
},
|
||||
"wechat": {
|
||||
"url": "https://mp.weixin.qq.com",
|
||||
"label": "微信公众号",
|
||||
"prefix": "微信",
|
||||
"aliases": ["公众号", "微信"],
|
||||
},
|
||||
"kimi": {
|
||||
"url": "https://kimi.moonshot.cn",
|
||||
"label": "Kimi",
|
||||
"aliases": ["月之暗面"],
|
||||
},
|
||||
"deepseek": {
|
||||
"url": "https://chat.deepseek.com",
|
||||
"label": "DeepSeek",
|
||||
},
|
||||
"doubao": {
|
||||
"url": "https://www.doubao.com",
|
||||
"label": "豆包",
|
||||
},
|
||||
"qianwen": {
|
||||
"url": "https://tongyi.aliyun.com",
|
||||
"label": "通义千问",
|
||||
"prefix": "通义",
|
||||
"aliases": ["通义", "千问"],
|
||||
},
|
||||
"yiyan": {
|
||||
"url": "https://yiyan.baidu.com",
|
||||
"label": "文心一言",
|
||||
"prefix": "文心",
|
||||
"aliases": ["文心", "一言"],
|
||||
},
|
||||
"yuanbao": {
|
||||
"url": "https://yuanbao.tencent.com",
|
||||
"label": "腾讯元宝",
|
||||
"prefix": "元宝",
|
||||
"aliases": ["元宝"],
|
||||
},
|
||||
}
|
||||
|
||||
# 未登录页共性:主行动点含「登录」——submit 按钮、Element 主按钮、Semi 文案、标题、通栏主按钮等,Playwright :has-text 可覆盖多数站点。
|
||||
_LOGIN_LOGGED_OUT_DOM_GENERIC_SELECTORS = (
|
||||
'button:has-text("登录")',
|
||||
'role=button[name="登录"]',
|
||||
'a:has-text("登录")',
|
||||
'h4:has-text("登录")',
|
||||
'span.semi-button-content:has-text("登录")',
|
||||
)
|
||||
|
||||
|
||||
def _build_platform_derived():
|
||||
urls = {}
|
||||
name_prefix = {}
|
||||
primary_cn = {}
|
||||
alias_to_key = {}
|
||||
|
||||
def _register_alias(alias: str, key: str) -> None:
|
||||
if not alias or not str(alias).strip():
|
||||
return
|
||||
a = str(alias).strip()
|
||||
if a not in alias_to_key:
|
||||
alias_to_key[a] = key
|
||||
lo = a.lower()
|
||||
if lo not in alias_to_key:
|
||||
alias_to_key[lo] = key
|
||||
|
||||
for key, spec in PLATFORMS.items():
|
||||
urls[key] = spec["url"]
|
||||
primary_cn[key] = spec["label"]
|
||||
name_prefix[key] = (spec.get("prefix") or spec["label"]).strip() or key
|
||||
_register_alias(key, key)
|
||||
_register_alias(spec["label"], key)
|
||||
for a in spec.get("aliases") or []:
|
||||
_register_alias(a, key)
|
||||
|
||||
return urls, name_prefix, primary_cn, alias_to_key
|
||||
|
||||
|
||||
PLATFORM_URLS, _PLATFORM_NAME_CN, _PLATFORM_PRIMARY_CN, _PLATFORM_ALIAS_TO_KEY = _build_platform_derived()
|
||||
|
||||
|
||||
def resolve_platform_key(name: str):
|
||||
"""将用户输入解析为内部 platform 键;无法识别返回 None。"""
|
||||
if name is None:
|
||||
return None
|
||||
s = str(name).strip()
|
||||
if not s:
|
||||
return None
|
||||
sl = s.lower()
|
||||
if sl in PLATFORM_URLS:
|
||||
return sl
|
||||
return _PLATFORM_ALIAS_TO_KEY.get(s)
|
||||
|
||||
|
||||
def _normalize_phone_digits(phone: str) -> str:
|
||||
"""从输入中提取数字串,供号段规则校验与入库去重(不对外承诺可随意夹杂符号)。"""
|
||||
d = re.sub(r"\D", "", (phone or "").strip())
|
||||
if d.startswith("86") and len(d) >= 13:
|
||||
d = d[2:]
|
||||
return d
|
||||
|
||||
|
||||
def _is_valid_cn_mobile11(digits: str) -> bool:
|
||||
"""中国大陆 11 位手机号:1 开头,第二位 3–9,共 11 位数字。"""
|
||||
return bool(re.fullmatch(r"1[3-9]\d{9}", digits or ""))
|
||||
|
||||
|
||||
def _platform_list_cn_for_help() -> str:
|
||||
"""帮助文案:一行中文展示名(不重复内部键)。"""
|
||||
parts = []
|
||||
for k in sorted(PLATFORM_URLS.keys()):
|
||||
parts.append(_PLATFORM_PRIMARY_CN.get(k, k))
|
||||
return "、".join(parts)
|
||||
100
scripts/util/runtime_paths.py
Normal file
100
scripts/util/runtime_paths.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""数据目录、profile 路径、CLI 路径调试输出。"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from util.constants import SKILL_SLUG
|
||||
from util.platforms import _PLATFORM_PRIMARY_CN
|
||||
|
||||
|
||||
def get_data_root():
|
||||
env = (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip()
|
||||
if env:
|
||||
return env
|
||||
if sys.platform == "win32":
|
||||
return r"D:\jiangchang-data"
|
||||
return os.path.join(os.path.expanduser("~"), ".jiangchang-data")
|
||||
|
||||
|
||||
def get_user_id():
|
||||
uid = (os.getenv("JIANGCHANG_USER_ID") or "").strip()
|
||||
return uid or "_anon"
|
||||
|
||||
|
||||
def get_skill_data_dir():
|
||||
path = os.path.join(get_data_root(), get_user_id(), SKILL_SLUG)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def get_skill_logs_dir() -> str:
|
||||
"""{DATA_ROOT}/{USER_ID}/account-manager/logs/"""
|
||||
path = os.path.join(get_skill_data_dir(), "logs")
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def get_db_path():
|
||||
return os.path.join(get_skill_data_dir(), "account-manager.db")
|
||||
|
||||
|
||||
def _runtime_paths_debug_text():
|
||||
"""供排查「为何 list 没数据」:终端未注入环境变量时会落到 _anon 等默认目录,与网关里用的库不是同一个文件。"""
|
||||
env_root = (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip() or "(未设置)"
|
||||
env_uid = (os.getenv("JIANGCHANG_USER_ID") or "").strip() or "(未设置→使用 _anon)"
|
||||
return (
|
||||
"[account-manager] "
|
||||
f"JIANGCHANG_DATA_ROOT={env_root} | "
|
||||
f"JIANGCHANG_USER_ID={env_uid} | "
|
||||
f"实际数据根={get_data_root()} | "
|
||||
f"实际用户目录={get_user_id()} | "
|
||||
f"数据库文件={get_db_path()}"
|
||||
)
|
||||
|
||||
|
||||
def _maybe_print_paths_debug_cli():
|
||||
"""设置 JIANGCHANG_ACCOUNT_DEBUG_PATHS=1 时,每次执行子命令前在 stderr 打印路径。"""
|
||||
v = (os.getenv("JIANGCHANG_ACCOUNT_DEBUG_PATHS") or "").strip().lower()
|
||||
if v in ("1", "true", "yes", "on"):
|
||||
print(_runtime_paths_debug_text(), file=sys.stderr)
|
||||
|
||||
|
||||
_WIN_FS_FORBIDDEN = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
|
||||
_WIN_RESERVED_NAMES = frozenset(
|
||||
["CON", "PRN", "AUX", "NUL"]
|
||||
+ [f"COM{i}" for i in range(1, 10)]
|
||||
+ [f"LPT{i}" for i in range(1, 10)]
|
||||
)
|
||||
|
||||
|
||||
def _fs_safe_segment(name: str, fallback: str) -> str:
|
||||
"""单级目录名:去掉 Windows 非法字符,避免末尾点/空格;空则用 fallback。"""
|
||||
t = _WIN_FS_FORBIDDEN.sub("_", (name or "").strip())
|
||||
t = t.rstrip(" .")
|
||||
if not t:
|
||||
t = fallback
|
||||
if t.upper() in _WIN_RESERVED_NAMES:
|
||||
t = f"_{t}"
|
||||
return t
|
||||
|
||||
|
||||
def get_default_profile_dir(
|
||||
platform_key: str, phone: Optional[str], account_id: Optional[int] = None
|
||||
) -> str:
|
||||
"""
|
||||
默认可读路径:profiles/<平台展示名>/<手机号>/(便于在资源管理器中辨认)。
|
||||
无手机号时退化为 profiles/<平台展示名>/no_phone_<id>/。
|
||||
"""
|
||||
label = _PLATFORM_PRIMARY_CN.get(platform_key, platform_key or "account")
|
||||
label_seg = _fs_safe_segment(label, _fs_safe_segment(platform_key or "", "platform"))
|
||||
ph = (phone or "").strip()
|
||||
if ph:
|
||||
phone_seg = _fs_safe_segment(ph, f"id_{account_id}" if account_id is not None else "phone")
|
||||
else:
|
||||
phone_seg = _fs_safe_segment(
|
||||
"", f"no_phone_{account_id}" if account_id is not None else "no_phone"
|
||||
)
|
||||
path = os.path.join(get_skill_data_dir(), "profiles", label_seg, phone_seg)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
Reference in New Issue
Block a user