Compare commits

...

3 Commits
v2.0.5 ... main

Author SHA1 Message Date
12b1fec0b9 feat: add requirements.txt with cryptography for host venv install
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-07-11 16:07:23 +08:00
f7f9555683 chore: sync SKILL.md version with release tag v2.0.6
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 16:23:34 +08:00
549869b2bb Release v2.0.1: create Chrome shortcut in profile dir on add-web
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-07-07 16:22:59 +08:00
6 changed files with 192 additions and 9 deletions

View File

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

@@ -0,0 +1,3 @@
# 公共依赖由宿主共享 runtime 提供;这里只声明技能特有 Python 依赖。
# Fernet 加密local_encrypted 凭据、master key、CLI 启动 import依赖此包。
cryptography>=42.0.0,<45

View File

@@ -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(

View File

@@ -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/")

View 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

View 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}"