Release v1.0.56: Credential & Browser Profile Manager
All checks were successful
技能自动化发布 / release (push) Successful in 7s
All checks were successful
技能自动化发布 / release (push) Successful in 7s
This commit is contained in:
@@ -1,145 +1,341 @@
|
||||
"""平台配置、别名解析、手机号校验。"""
|
||||
import re
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Platform definitions and resolution.
|
||||
|
||||
# 平台唯一配置表:只改这里即可。键 = 入库的 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": ["搜狐"],
|
||||
Central authoritative registry for all platforms (logistics, LLM, content, etc.).
|
||||
Import and use PLATFORMS dict, resolve_platform_key(), and seed_platforms().
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
|
||||
from util.logging_config import get_skill_logger
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Platform specification fields:
|
||||
# display_name – Human-readable name (e.g. "Maersk")
|
||||
# domain – Business domain: logistics / llm / content / ecommerce / generic
|
||||
# provider_code – Supplier code (logistics); can be same as key
|
||||
# default_url – Default entry URL (empty string if none)
|
||||
# aliases – List of name aliases (Chinese/English, for CLI resolution)
|
||||
# capabilities – Dict of capability flags:
|
||||
# {"web": bool, "api_key": bool, "rpa": bool}
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
PLATFORMS: dict[str, dict[str, Any]] = {
|
||||
# =========================================================================
|
||||
# Logistics platforms
|
||||
# =========================================================================
|
||||
"maersk": {
|
||||
"display_name": "Maersk",
|
||||
"domain": "logistics",
|
||||
"provider_code": "maersk",
|
||||
"default_url": "https://www.maersk.com",
|
||||
"aliases": ["马士基", "maersk", "Maersk"],
|
||||
"capabilities": {"web": True, "api_key": False, "rpa": True},
|
||||
},
|
||||
"toutiao": {
|
||||
"url": "https://mp.toutiao.com/",
|
||||
"label": "头条号",
|
||||
"prefix": "头条",
|
||||
"aliases": ["头条"],
|
||||
"cosco": {
|
||||
"display_name": "COSCO",
|
||||
"domain": "logistics",
|
||||
"provider_code": "cosco",
|
||||
"default_url": "",
|
||||
"aliases": ["中远海运", "COSCO", "cosco"],
|
||||
"capabilities": {"web": True, "api_key": False, "rpa": True},
|
||||
},
|
||||
"zhihu": {
|
||||
"url": "https://www.zhihu.com",
|
||||
"label": "知乎",
|
||||
"aliases": ["知乎号"],
|
||||
"msc": {
|
||||
"display_name": "MSC",
|
||||
"domain": "logistics",
|
||||
"provider_code": "msc",
|
||||
"default_url": "",
|
||||
"aliases": ["MSC", "地中海航运"],
|
||||
"capabilities": {"web": True, "api_key": False, "rpa": True},
|
||||
},
|
||||
"wechat": {
|
||||
"url": "https://mp.weixin.qq.com",
|
||||
"label": "微信公众号",
|
||||
"prefix": "微信",
|
||||
"aliases": ["公众号", "微信"],
|
||||
"cma_cgm": {
|
||||
"display_name": "CMA CGM",
|
||||
"domain": "logistics",
|
||||
"provider_code": "cma_cgm",
|
||||
"default_url": "",
|
||||
"aliases": ["达飞", "CMA CGM", "cma cgm"],
|
||||
"capabilities": {"web": True, "api_key": False, "rpa": True},
|
||||
},
|
||||
"kimi": {
|
||||
"url": "https://kimi.moonshot.cn",
|
||||
"label": "Kimi",
|
||||
"aliases": ["月之暗面"],
|
||||
"evergreen": {
|
||||
"display_name": "Evergreen",
|
||||
"domain": "logistics",
|
||||
"provider_code": "evergreen",
|
||||
"default_url": "",
|
||||
"aliases": ["长荣", "Evergreen", "evergreen"],
|
||||
"capabilities": {"web": True, "api_key": False, "rpa": True},
|
||||
},
|
||||
"cargo_wise": {
|
||||
"display_name": "CargoWise",
|
||||
"domain": "logistics",
|
||||
"provider_code": "cargo_wise",
|
||||
"default_url": "",
|
||||
"aliases": ["CargoWise", "Cargo Wise", "cargo wise"],
|
||||
"capabilities": {"web": True, "api_key": False, "rpa": True},
|
||||
},
|
||||
"freightos": {
|
||||
"display_name": "Freightos",
|
||||
"domain": "logistics",
|
||||
"provider_code": "freightos",
|
||||
"default_url": "",
|
||||
"aliases": ["Freightos", "freightos"],
|
||||
"capabilities": {"web": True, "api_key": True, "rpa": False},
|
||||
},
|
||||
"webcargo": {
|
||||
"display_name": "WebCargo",
|
||||
"domain": "logistics",
|
||||
"provider_code": "webcargo",
|
||||
"default_url": "",
|
||||
"aliases": ["WebCargo", "web cargo"],
|
||||
"capabilities": {"web": True, "api_key": True, "rpa": False},
|
||||
},
|
||||
"sinotrans": {
|
||||
"display_name": "Sinotrans",
|
||||
"domain": "logistics",
|
||||
"provider_code": "sinotrans",
|
||||
"default_url": "",
|
||||
"aliases": ["中外运", "Sinotrans", "sinotrans"],
|
||||
"capabilities": {"web": True, "api_key": False, "rpa": True},
|
||||
},
|
||||
# =========================================================================
|
||||
# LLM / AI platforms
|
||||
# =========================================================================
|
||||
"deepseek": {
|
||||
"url": "https://chat.deepseek.com",
|
||||
"label": "DeepSeek",
|
||||
"display_name": "DeepSeek",
|
||||
"domain": "llm",
|
||||
"provider_code": "deepseek",
|
||||
"default_url": "https://chat.deepseek.com",
|
||||
"aliases": ["DeepSeek", "deepseek"],
|
||||
"capabilities": {"web": True, "api_key": True, "rpa": False},
|
||||
},
|
||||
"doubao": {
|
||||
"url": "https://www.doubao.com",
|
||||
"label": "豆包",
|
||||
"display_name": "Doubao",
|
||||
"domain": "llm",
|
||||
"provider_code": "doubao",
|
||||
"default_url": "https://www.doubao.com",
|
||||
"aliases": ["豆包", "Doubao"],
|
||||
"capabilities": {"web": True, "api_key": True, "rpa": False},
|
||||
},
|
||||
"kimi": {
|
||||
"display_name": "Kimi",
|
||||
"domain": "llm",
|
||||
"provider_code": "kimi",
|
||||
"default_url": "https://kimi.moonshot.cn",
|
||||
"aliases": ["Kimi", "月之暗面"],
|
||||
"capabilities": {"web": True, "api_key": True, "rpa": False},
|
||||
},
|
||||
"qianwen": {
|
||||
"url": "https://tongyi.aliyun.com",
|
||||
"label": "通义千问",
|
||||
"prefix": "通义",
|
||||
"aliases": ["通义", "千问"],
|
||||
"display_name": "Qianwen",
|
||||
"domain": "llm",
|
||||
"provider_code": "qianwen",
|
||||
"default_url": "https://tongyi.aliyun.com",
|
||||
"aliases": ["通义千问", "通义", "千问", "Qianwen"],
|
||||
"capabilities": {"web": True, "api_key": True, "rpa": False},
|
||||
},
|
||||
"yiyan": {
|
||||
"url": "https://yiyan.baidu.com",
|
||||
"label": "文心一言",
|
||||
"prefix": "文心",
|
||||
"aliases": ["文心", "一言"],
|
||||
"display_name": "YiYan",
|
||||
"domain": "llm",
|
||||
"provider_code": "yiyan",
|
||||
"default_url": "https://yiyan.baidu.com",
|
||||
"aliases": ["文心一言", "文心", "一言", "YiYan"],
|
||||
"capabilities": {"web": True, "api_key": True, "rpa": False},
|
||||
},
|
||||
"yuanbao": {
|
||||
"url": "https://yuanbao.tencent.com",
|
||||
"label": "腾讯元宝",
|
||||
"prefix": "元宝",
|
||||
"aliases": ["元宝"],
|
||||
"display_name": "Yuanbao",
|
||||
"domain": "llm",
|
||||
"provider_code": "yuanbao",
|
||||
"default_url": "https://yuanbao.tencent.com",
|
||||
"aliases": ["腾讯元宝", "元宝", "Yuanbao"],
|
||||
"capabilities": {"web": True, "api_key": True, "rpa": False},
|
||||
},
|
||||
"gemini": {
|
||||
"url": "https://gemini.google.com",
|
||||
"label": "Gemini",
|
||||
"prefix": "Gemini",
|
||||
"aliases": ["谷歌Gemini", "Google Gemini", "google gemini", "Bard"],
|
||||
"display_name": "Gemini",
|
||||
"domain": "llm",
|
||||
"provider_code": "gemini",
|
||||
"default_url": "https://gemini.google.com",
|
||||
"aliases": ["Gemini", "谷歌Gemini", "Google Gemini", "Bard"],
|
||||
"capabilities": {"web": True, "api_key": True, "rpa": False},
|
||||
},
|
||||
"minimax": {
|
||||
"display_name": "MiniMax",
|
||||
"domain": "llm",
|
||||
"provider_code": "minimax",
|
||||
"default_url": "",
|
||||
"aliases": ["MiniMax", "minimax"],
|
||||
"capabilities": {"web": True, "api_key": True, "rpa": False},
|
||||
},
|
||||
# =========================================================================
|
||||
# Content platforms (carried forward from v1)
|
||||
# =========================================================================
|
||||
"sohu": {
|
||||
"display_name": "搜狐号",
|
||||
"domain": "content",
|
||||
"provider_code": "sohu",
|
||||
"default_url": "https://mp.sohu.com",
|
||||
"aliases": ["搜狐", "搜狐号"],
|
||||
"capabilities": {"web": True, "api_key": False, "rpa": False},
|
||||
},
|
||||
"toutiao": {
|
||||
"display_name": "头条号",
|
||||
"domain": "content",
|
||||
"provider_code": "toutiao",
|
||||
"default_url": "https://mp.toutiao.com/",
|
||||
"aliases": ["头条", "头条号"],
|
||||
"capabilities": {"web": True, "api_key": False, "rpa": False},
|
||||
},
|
||||
"zhihu": {
|
||||
"display_name": "知乎",
|
||||
"domain": "content",
|
||||
"provider_code": "zhihu",
|
||||
"default_url": "https://www.zhihu.com",
|
||||
"aliases": ["知乎", "知乎号"],
|
||||
"capabilities": {"web": True, "api_key": False, "rpa": False},
|
||||
},
|
||||
"wechat": {
|
||||
"display_name": "微信公众号",
|
||||
"domain": "content",
|
||||
"provider_code": "wechat",
|
||||
"default_url": "https://mp.weixin.qq.com",
|
||||
"aliases": ["公众号", "微信", "微信公众号"],
|
||||
"capabilities": {"web": True, "api_key": False, "rpa": False},
|
||||
},
|
||||
}
|
||||
|
||||
# 未登录页共性:主行动点含「登录」——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
|
||||
# ---------------------------------------------------------------------------
|
||||
# Derived alias map
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _build_alias_map() -> dict[str, str]:
|
||||
"""Build a case-insensitive flat alias -> key map."""
|
||||
m: dict[str, str] = {}
|
||||
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
|
||||
# Register the key itself (lowercase)
|
||||
m[key.lower()] = key
|
||||
# Register all aliases (original + lowercase)
|
||||
for alias in [spec["display_name"]] + spec.get("aliases", []):
|
||||
a = str(alias).strip()
|
||||
if a:
|
||||
m[a] = key
|
||||
m[a.lower()] = key
|
||||
return m
|
||||
|
||||
|
||||
PLATFORM_URLS, _PLATFORM_NAME_CN, _PLATFORM_PRIMARY_CN, _PLATFORM_ALIAS_TO_KEY = _build_platform_derived()
|
||||
_PLATFORM_ALIAS_MAP = _build_alias_map()
|
||||
|
||||
|
||||
def resolve_platform_key(name: str):
|
||||
"""将用户输入解析为内部 platform 键;无法识别返回 None。"""
|
||||
if name is None:
|
||||
def resolve_platform_key(name: str) -> Optional[str]:
|
||||
"""
|
||||
Resolve user input to a canonical platform key.
|
||||
Supports: key, display_name, any alias. Case-insensitive.
|
||||
Returns None if unrecognized.
|
||||
"""
|
||||
if not name:
|
||||
return None
|
||||
s = str(name).strip()
|
||||
if not s:
|
||||
return None
|
||||
sl = s.lower()
|
||||
if sl in PLATFORM_URLS:
|
||||
# Direct key match
|
||||
if sl in PLATFORMS:
|
||||
return sl
|
||||
return _PLATFORM_ALIAS_TO_KEY.get(s)
|
||||
# Alias map
|
||||
if sl in _PLATFORM_ALIAS_MAP:
|
||||
return _PLATFORM_ALIAS_MAP[sl]
|
||||
if s in _PLATFORM_ALIAS_MAP:
|
||||
return _PLATFORM_ALIAS_MAP[s]
|
||||
return None
|
||||
|
||||
|
||||
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 get_platform(key: str) -> Optional[dict[str, Any]]:
|
||||
"""Return the full platform spec dict for a validated key."""
|
||||
return PLATFORMS.get(key)
|
||||
|
||||
|
||||
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 list_platforms(domain: Optional[str] = None) -> list[dict[str, Any]]:
|
||||
"""List all platforms, optionally filtered by domain.
|
||||
Returns a list of dicts with metadata."""
|
||||
result = []
|
||||
for key, spec in PLATFORMS.items():
|
||||
if domain and spec["domain"] != domain:
|
||||
continue
|
||||
result.append({
|
||||
"platform_key": key,
|
||||
"display_name": spec["display_name"],
|
||||
"domain": spec["domain"],
|
||||
"provider_code": spec.get("provider_code"),
|
||||
"default_url": spec.get("default_url", ""),
|
||||
"aliases": spec.get("aliases", []),
|
||||
"capabilities": spec.get("capabilities", {}),
|
||||
"enabled": True,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def seed_platforms(conn) -> None:
|
||||
"""
|
||||
Upsert all platform definitions into the 'platforms' table.
|
||||
Must be called after init_db() in the same connection lifecycle.
|
||||
"""
|
||||
log = get_skill_logger()
|
||||
log.info("platform_seed_start")
|
||||
cur = conn.cursor()
|
||||
now = int(time.time())
|
||||
upsert_count = 0
|
||||
for key, spec in PLATFORMS.items():
|
||||
aliases_json = json.dumps(spec.get("aliases", []), ensure_ascii=False)
|
||||
capabilities_json = json.dumps(spec.get("capabilities", {}), ensure_ascii=False)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO platforms (platform_key, display_name, domain, provider_code,
|
||||
default_url, aliases_json, capabilities_json,
|
||||
enabled, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
||||
ON CONFLICT(platform_key) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
domain = excluded.domain,
|
||||
provider_code = excluded.provider_code,
|
||||
default_url = excluded.default_url,
|
||||
aliases_json = excluded.aliases_json,
|
||||
capabilities_json = excluded.capabilities_json,
|
||||
enabled = 1,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(
|
||||
key,
|
||||
spec["display_name"],
|
||||
spec["domain"],
|
||||
spec.get("provider_code"),
|
||||
spec.get("default_url", ""),
|
||||
aliases_json,
|
||||
capabilities_json,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
upsert_count += 1
|
||||
conn.commit()
|
||||
log.info("platform_seed_done count=%s", upsert_count)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Legacy compatibility helpers (for old account_service v1 code references)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Kept for backward compatibility so old imports don't break immediately.
|
||||
# New code should use PLATFORMS dict directly and resolve_platform_key().
|
||||
_PLATFORM_PRIMARY_CN: dict[str, str] = {
|
||||
k: spec["display_name"] for k, spec in PLATFORMS.items()
|
||||
}
|
||||
PLATFORM_URLS: dict[str, str] = {
|
||||
k: (spec.get("default_url") or "") for k, spec in PLATFORMS.items()
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
"""帮助文案:一行展示名,供 CLI 用法提示。"""
|
||||
return "、".join(
|
||||
spec["display_name"] for _k, spec in sorted(
|
||||
PLATFORMS.items(), key=lambda x: x[1]["display_name"]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""数据目录、profile 路径、CLI 路径调试输出。"""
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Data directory, profile path, CLI path debugging output.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -6,21 +9,20 @@ from typing import Optional
|
||||
|
||||
from jiangchang_skill_core.runtime_env import get_data_root, get_user_id
|
||||
from util.constants import SKILL_SLUG
|
||||
from util.platforms import _PLATFORM_PRIMARY_CN
|
||||
|
||||
|
||||
def get_skill_data_dir():
|
||||
def get_skill_data_dir() -> str:
|
||||
path = os.path.join(get_data_root(), get_user_id(), SKILL_SLUG)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def get_db_path():
|
||||
def get_db_path() -> str:
|
||||
return os.path.join(get_skill_data_dir(), "account-manager.db")
|
||||
|
||||
|
||||
def _runtime_paths_debug_text():
|
||||
"""供排查「为何 list 没数据」:终端未注入环境变量时会落到 _anon 等默认目录,与网关里用的库不是同一个文件。"""
|
||||
def _runtime_paths_debug_text() -> str:
|
||||
"""For debugging: print current data path info to stderr."""
|
||||
env_root = (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip() or "(未设置)"
|
||||
env_uid = (os.getenv("JIANGCHANG_USER_ID") or "").strip() or "(未设置→使用 _anon)"
|
||||
return (
|
||||
@@ -33,8 +35,8 @@ def _runtime_paths_debug_text():
|
||||
)
|
||||
|
||||
|
||||
def _maybe_print_paths_debug_cli():
|
||||
"""设置 JIANGCHANG_ACCOUNT_DEBUG_PATHS=1 时,每次执行子命令前在 stderr 打印路径。"""
|
||||
def _maybe_print_paths_debug_cli() -> None:
|
||||
"""Set JIANGCHANG_ACCOUNT_DEBUG_PATHS=1 to print paths in 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)
|
||||
@@ -49,7 +51,7 @@ _WIN_RESERVED_NAMES = frozenset(
|
||||
|
||||
|
||||
def _fs_safe_segment(name: str, fallback: str) -> str:
|
||||
"""单级目录名:去掉 Windows 非法字符,避免末尾点/空格;空则用 fallback。"""
|
||||
"""Single directory segment: strip Windows-unsafe chars; empty -> fallback."""
|
||||
t = _WIN_FS_FORBIDDEN.sub("_", (name or "").strip())
|
||||
t = t.rstrip(" .")
|
||||
if not t:
|
||||
@@ -59,22 +61,33 @@ def _fs_safe_segment(name: str, fallback: str) -> str:
|
||||
return t
|
||||
|
||||
|
||||
def get_default_profile_dir(
|
||||
platform_key: str, phone: Optional[str], account_id: Optional[int] = None
|
||||
def get_default_profile_dir_for_web(
|
||||
platform_key: str,
|
||||
label: str,
|
||||
login_id: Optional[str] = None,
|
||||
domain: str = "generic",
|
||||
) -> str:
|
||||
"""
|
||||
默认可读路径:profiles/<平台展示名>/<手机号>/(便于在资源管理器中辨认)。
|
||||
无手机号时退化为 profiles/<平台展示名>/no_phone_<id>/。
|
||||
Generate a browser profile directory path.
|
||||
|
||||
Structure:
|
||||
{DATA_ROOT}/{USER_ID}/account-manager/profiles/{domain}/{platform_key}/{safe_label}[_{login_id_suffix]/
|
||||
|
||||
This keeps profiles organized by domain and platform for easy
|
||||
identification in file explorer.
|
||||
"""
|
||||
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)
|
||||
label_seg = _fs_safe_segment(label, platform_key)
|
||||
login_suffix = ""
|
||||
if login_id:
|
||||
safe_login = _fs_safe_segment(login_id, "")
|
||||
if safe_login:
|
||||
login_suffix = f"_{safe_login[:16]}"
|
||||
path = os.path.join(
|
||||
get_skill_data_dir(),
|
||||
"profiles",
|
||||
_fs_safe_segment(domain, "generic"),
|
||||
_fs_safe_segment(platform_key, "unknown"),
|
||||
f"{label_seg}{login_suffix}",
|
||||
)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
Reference in New Issue
Block a user