Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12b1fec0b9 | |||
| f7f9555683 | |||
| 549869b2bb |
2
SKILL.md
2
SKILL.md
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: 账号与凭据管理
|
name: 账号与凭据管理
|
||||||
description: "通用账号 & 凭据 & 登录流程管理器。管理网页/RPA 账号、API Key/Token;支持 9 种登录策略(账密自动、扫码、2FA、SSO 等);提供加密本地存储(Fernet);提供 rpa_helpers 库供业务 skill 同进程调用 ensure_logged_in 完成多平台自动登录。涵盖物流、LLM、内容、开发、API 五类平台。"
|
description: "通用账号 & 凭据 & 登录流程管理器。管理网页/RPA 账号、API Key/Token;支持 9 种登录策略(账密自动、扫码、2FA、SSO 等);提供加密本地存储(Fernet);提供 rpa_helpers 库供业务 skill 同进程调用 ensure_logged_in 完成多平台自动登录。涵盖物流、LLM、内容、开发、API 五类平台。"
|
||||||
version: 2.0.0
|
version: 2.0.7
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
|
|||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# 公共依赖由宿主共享 runtime 提供;这里只声明技能特有 Python 依赖。
|
||||||
|
# Fernet 加密(local_encrypted 凭据、master key、CLI 启动 import)依赖此包。
|
||||||
|
cryptography>=42.0.0,<45
|
||||||
@@ -38,6 +38,7 @@ from service.secret_store import (
|
|||||||
)
|
)
|
||||||
from util.logging_config import get_skill_logger
|
from util.logging_config import get_skill_logger
|
||||||
from util.platforms import get_platform_spec, seed_platforms
|
from util.platforms import get_platform_spec, seed_platforms
|
||||||
|
from util.profile_shortcut import create_profile_browser_shortcut
|
||||||
from util.runtime_paths import get_default_profile_dir_for_web
|
from util.runtime_paths import get_default_profile_dir_for_web
|
||||||
|
|
||||||
|
|
||||||
@@ -160,8 +161,10 @@ def cmd_account_add_web(
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
shortcut_path = create_profile_browser_shortcut(resolved_profile_dir, safe_label, log)
|
||||||
|
|
||||||
log.info("account_add_web_success id=%s platform=%s profile_dir=%s", new_id, key, resolved_profile_dir)
|
log.info("account_add_web_success id=%s platform=%s profile_dir=%s", new_id, key, resolved_profile_dir)
|
||||||
_say(_ok_json({
|
ok_payload = {
|
||||||
"id": new_id,
|
"id": new_id,
|
||||||
"platform_key": key,
|
"platform_key": key,
|
||||||
"account_label": safe_label,
|
"account_label": safe_label,
|
||||||
@@ -176,7 +179,10 @@ def cmd_account_add_web(
|
|||||||
"auth_strategy": resolved_auth,
|
"auth_strategy": resolved_auth,
|
||||||
"session_persistent": sp,
|
"session_persistent": sp,
|
||||||
"device_fingerprint": device_fingerprint,
|
"device_fingerprint": device_fingerprint,
|
||||||
}))
|
}
|
||||||
|
if shortcut_path:
|
||||||
|
ok_payload["profile_shortcut"] = shortcut_path
|
||||||
|
_say(_ok_json(ok_payload))
|
||||||
|
|
||||||
|
|
||||||
def cmd_account_add_secret(
|
def cmd_account_add_secret(
|
||||||
|
|||||||
@@ -33,13 +33,12 @@ def _win_find_exe(candidates):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def resolve_chromium_channel():
|
def resolve_chromium_executable():
|
||||||
"""
|
"""
|
||||||
Playwright channel: 'chrome' | 'msedge' | None
|
Windows 上返回 chrome.exe 或 msedge.exe 的绝对路径;未找到或非 Windows 返回 None。
|
||||||
Windows 优先 Chrome,其次 Edge;其它系统默认 chrome(由 Playwright 解析 PATH)。
|
|
||||||
"""
|
"""
|
||||||
if sys.platform != "win32":
|
if sys.platform != "win32":
|
||||||
return "chrome"
|
return None
|
||||||
|
|
||||||
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
|
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
|
||||||
pfx86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
pfx86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
||||||
@@ -53,7 +52,7 @@ def resolve_chromium_channel():
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
if chrome:
|
if chrome:
|
||||||
return "chrome"
|
return chrome
|
||||||
|
|
||||||
edge = _win_find_exe(
|
edge = _win_find_exe(
|
||||||
[
|
[
|
||||||
@@ -63,11 +62,27 @@ def resolve_chromium_channel():
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
if edge:
|
if edge:
|
||||||
return "msedge"
|
return edge
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_chromium_channel():
|
||||||
|
"""
|
||||||
|
Playwright channel: 'chrome' | 'msedge' | None
|
||||||
|
Windows 优先 Chrome,其次 Edge;其它系统默认 chrome(由 Playwright 解析 PATH)。
|
||||||
|
"""
|
||||||
|
if sys.platform != "win32":
|
||||||
|
return "chrome"
|
||||||
|
|
||||||
|
exe = resolve_chromium_executable()
|
||||||
|
if not exe:
|
||||||
|
return None
|
||||||
|
if os.path.basename(exe).lower() == "msedge.exe":
|
||||||
|
return "msedge"
|
||||||
|
return "chrome"
|
||||||
|
|
||||||
|
|
||||||
def _print_browser_install_hint():
|
def _print_browser_install_hint():
|
||||||
print("❌ 未检测到 Google Chrome 或 Microsoft Edge,请先安装:")
|
print("❌ 未检测到 Google Chrome 或 Microsoft Edge,请先安装:")
|
||||||
print(" • Chrome: https://www.google.com/chrome/")
|
print(" • Chrome: https://www.google.com/chrome/")
|
||||||
|
|||||||
84
scripts/util/profile_shortcut.py
Normal file
84
scripts/util/profile_shortcut.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""在 Profile 目录内创建浏览器快捷方式(Windows .lnk)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from service.browser_service import resolve_chromium_executable
|
||||||
|
|
||||||
|
_WIN_LNK_FORBIDDEN = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
|
||||||
|
|
||||||
|
|
||||||
|
def _lnk_safe_filename(name: str, fallback: str = "browser") -> str:
|
||||||
|
t = _WIN_LNK_FORBIDDEN.sub("_", (name or "").strip())
|
||||||
|
t = t.rstrip(" .")
|
||||||
|
return t or fallback
|
||||||
|
|
||||||
|
|
||||||
|
def _ps_single_quote(value: str) -> str:
|
||||||
|
return "'" + value.replace("'", "''") + "'"
|
||||||
|
|
||||||
|
|
||||||
|
def create_profile_browser_shortcut(
|
||||||
|
profile_dir: str,
|
||||||
|
account_label: str,
|
||||||
|
log: Optional[logging.Logger] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
在 profile_dir 内创建 {account_label}.lnk,目标为系统 Chrome/Edge + --user-data-dir。
|
||||||
|
|
||||||
|
非 Windows 或未安装 Chrome/Edge 时静默跳过,返回 None。
|
||||||
|
成功返回 .lnk 绝对路径。
|
||||||
|
"""
|
||||||
|
if log is None:
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
if sys.platform != "win32":
|
||||||
|
log.debug("profile_shortcut_skip platform=%s", sys.platform)
|
||||||
|
return None
|
||||||
|
|
||||||
|
browser_exe = resolve_chromium_executable()
|
||||||
|
if not browser_exe:
|
||||||
|
log.warning("profile_shortcut_skip reason=no_chromium")
|
||||||
|
return None
|
||||||
|
|
||||||
|
profile_abs = os.path.abspath(profile_dir)
|
||||||
|
os.makedirs(profile_abs, exist_ok=True)
|
||||||
|
|
||||||
|
lnk_name = f"{_lnk_safe_filename(account_label)}.lnk"
|
||||||
|
lnk_path = os.path.join(profile_abs, lnk_name)
|
||||||
|
arguments = f'--user-data-dir="{profile_abs}"'
|
||||||
|
working_dir = os.path.dirname(browser_exe)
|
||||||
|
|
||||||
|
ps = (
|
||||||
|
"$WshShell = New-Object -ComObject WScript.Shell; "
|
||||||
|
f"$s = $WshShell.CreateShortcut({_ps_single_quote(lnk_path)}); "
|
||||||
|
f"$s.TargetPath = {_ps_single_quote(browser_exe)}; "
|
||||||
|
f"$s.Arguments = {_ps_single_quote(arguments)}; "
|
||||||
|
f"$s.IconLocation = {_ps_single_quote(browser_exe + ',0')}; "
|
||||||
|
f"$s.WorkingDirectory = {_ps_single_quote(working_dir)}; "
|
||||||
|
"$s.Save()"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["powershell", "-NoProfile", "-NonInteractive", "-Command", ps],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.CalledProcessError) as exc:
|
||||||
|
log.warning("profile_shortcut_failed profile_dir=%s error=%s", profile_abs, exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not os.path.isfile(lnk_path):
|
||||||
|
log.warning("profile_shortcut_missing_after_create path=%s", lnk_path)
|
||||||
|
return None
|
||||||
|
|
||||||
|
log.info("profile_shortcut_created path=%s browser=%s", lnk_path, browser_exe)
|
||||||
|
return lnk_path
|
||||||
75
tests/test_profile_shortcut.py
Normal file
75
tests/test_profile_shortcut.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Profile 目录浏览器快捷方式测试。"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from util.profile_shortcut import _lnk_safe_filename, create_profile_browser_shortcut
|
||||||
|
|
||||||
|
|
||||||
|
class TestLnkSafeFilename:
|
||||||
|
def test_strips_forbidden_chars(self) -> None:
|
||||||
|
assert _lnk_safe_filename('小红书/测试:账号') == "小红书_测试_账号"
|
||||||
|
|
||||||
|
def test_empty_uses_fallback(self) -> None:
|
||||||
|
assert _lnk_safe_filename(" ") == "browser"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateProfileBrowserShortcut:
|
||||||
|
def test_skips_non_windows(self, tmp_path, monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr("util.profile_shortcut.sys.platform", "linux")
|
||||||
|
out = create_profile_browser_shortcut(str(tmp_path), "demo")
|
||||||
|
assert out is None
|
||||||
|
|
||||||
|
def test_skips_when_no_browser(self, tmp_path, monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr("util.profile_shortcut.sys.platform", "win32")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"util.profile_shortcut.resolve_chromium_executable",
|
||||||
|
lambda: None,
|
||||||
|
)
|
||||||
|
out = create_profile_browser_shortcut(str(tmp_path), "demo")
|
||||||
|
assert out is None
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.platform != "win32", reason="Windows .lnk only")
|
||||||
|
def test_creates_lnk_on_windows(self, tmp_path, monkeypatch) -> None:
|
||||||
|
from service.browser_service import resolve_chromium_executable
|
||||||
|
|
||||||
|
browser_exe = resolve_chromium_executable()
|
||||||
|
if not browser_exe:
|
||||||
|
pytest.skip("no Chrome/Edge installed")
|
||||||
|
|
||||||
|
label = "ShortcutTest"
|
||||||
|
out = create_profile_browser_shortcut(str(tmp_path), label)
|
||||||
|
assert out is not None
|
||||||
|
assert os.path.isfile(out)
|
||||||
|
assert out.endswith(f"{label}.lnk")
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.platform != "win32", reason="Windows .lnk only")
|
||||||
|
def test_add_web_creates_shortcut(self, clean_db, fake_env, monkeypatch) -> None:
|
||||||
|
from service.browser_service import resolve_chromium_executable
|
||||||
|
from service.account_service import cmd_account_add_web
|
||||||
|
from db.accounts_repo import find_accounts
|
||||||
|
|
||||||
|
if not resolve_chromium_executable():
|
||||||
|
pytest.skip("no Chrome/Edge installed")
|
||||||
|
|
||||||
|
label = "Maersk shortcut test"
|
||||||
|
cmd_account_add_web(
|
||||||
|
platform_input="maersk",
|
||||||
|
login_id="shortcut@test.com",
|
||||||
|
login_id_type="email",
|
||||||
|
label=label,
|
||||||
|
tenant_id="tenant-test",
|
||||||
|
environment="production",
|
||||||
|
role="booking",
|
||||||
|
provider_code=None,
|
||||||
|
url=None,
|
||||||
|
extra_json_str=None,
|
||||||
|
profile_dir_override=None,
|
||||||
|
)
|
||||||
|
accs = find_accounts(platform_key="maersk")
|
||||||
|
assert len(accs) == 1
|
||||||
|
profile_dir = accs[0]["profile_dir"]
|
||||||
|
lnk_path = os.path.join(profile_dir, f"{label}.lnk")
|
||||||
|
assert os.path.isfile(lnk_path), f"expected shortcut at {lnk_path}"
|
||||||
Reference in New Issue
Block a user