Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 097dde0ebb | |||
| 718aeefd44 |
6
SKILL.md
6
SKILL.md
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 账号与凭据管理
|
||||
description: "通用账号 & 凭据 & 登录流程管理器。管理网页/RPA 账号、API Key/Token;支持 9 种登录策略(账密自动、扫码、2FA、SSO 等);提供加密本地存储(Fernet);提供 rpa_helpers 库供业务 skill 同进程调用 ensure_logged_in 完成多平台自动登录。涵盖物流、LLM、内容、开发、API 五类平台。"
|
||||
version: 2.0.20
|
||||
version: 2.0.22
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
@@ -34,8 +34,8 @@ allowed-tools:
|
||||
5. **用人话定位账号**:`account resolve --login-id <手机号/用户名>` 或 `--label` / `--query`(不必先查数字 id)。
|
||||
6. **为 RPA 挑选账号**:`account pick-web` — 按平台/环境/角色,也可 `--login-id` / `--label`;业务首启推荐 `account ensure-web`(或 `pick-web --ensure`):无账号则自动 `add-web` 再返回。
|
||||
7. **获取 API Key(凭据表)**:`credential pick` — 默认隐藏 secret,`--reveal` 时才返回。
|
||||
8. **浏览器打开查看**:`account open <id>`(或兼容别名 `open <id>`)— **本进程**直接拉起系统 Chrome/Edge(`--user-data-dir`),与「打开文件位置」同模式;不做登录判定。数据管理行内按钮均为 `interaction: direct` + `sync`。
|
||||
9. **打开 Profile 目录**:`account reveal-profile <id>` — 在系统文件管理器中打开该账号 `profile_dir`。
|
||||
8. **打开浏览器**:`account open <id>`(或兼容别名 `open <id>`)— **本进程**直接拉起系统 Chrome/Edge(独立 `--user-data-dir`),最大化并跳过 Chrome 首次欢迎页,直接打开平台链接;与「打开目录」同为一点即走,不做登录判定。数据管理行内按钮均为 `interaction: direct` + `sync`,成功不弹提示。
|
||||
9. **打开目录**:`account reveal-profile <id>` — 在系统文件管理器中打开该账号 `profile_dir`。
|
||||
10. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。
|
||||
11. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。
|
||||
12. **加密备份与迁移**:`account export-credentials --plaintext` / `account import-credentials` — 导出明文 JSON、在新环境重新加密写入。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# assets
|
||||
|
||||
- `actions.json`:匠厂宿主 Skill Action 清单(数据管理「账号信息」行内:打开文件位置 / 打开浏览器)。
|
||||
- `actions.json`:匠厂宿主 Skill Action 清单(数据管理「账号信息」行内:打开目录 / 打开浏览器)。
|
||||
- `examples/`:CLI 成功输出形状示例(虚构路径与数据)。
|
||||
- `schemas/`:`get` / `list-json` / `pick-web` 单条对象的轻量 JSON Schema(`additionalProperties: true` 允许扩展字段)。
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
},
|
||||
{
|
||||
"id": "reveal-profile",
|
||||
"label": "打开文件位置",
|
||||
"label": "打开目录",
|
||||
"description": "在系统文件管理器中打开该账号的浏览器 Profile 目录。",
|
||||
"placements": ["row"],
|
||||
"executionProfile": "sync",
|
||||
@@ -108,7 +108,7 @@
|
||||
{
|
||||
"id": "open-browser",
|
||||
"label": "打开浏览器",
|
||||
"description": "用该账号持久化 Profile 直接打开系统 Chrome/Edge,仅供肉眼核对(不做登录判定)。与打开文件位置同为一点即开。",
|
||||
"description": "用该账号持久化 Profile 直接打开系统 Chrome/Edge,仅供肉眼核对(不做登录判定)。与打开目录同为一点即开。",
|
||||
"placements": ["row"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "direct",
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
| `account mark-session <id> --session-status <status>` | 标记会话状态 |
|
||||
| `account mark-error <id> --code <code> --message <msg>` | 标记错误 |
|
||||
| `account mark-used <id>` | 标记已使用 |
|
||||
| `account open <id>` | 本进程直接打开系统 Chrome/Edge(`--user-data-dir=profile`;无额外 python 子进程) |
|
||||
| `account open <id>` | 本进程直接打开系统 Chrome/Edge(独立 profile;最大化;跳过首次欢迎页;打开平台 URL) |
|
||||
| `account reveal-profile <id>` | 在系统文件管理器中打开该账号 `profile_dir` |
|
||||
|
||||
### D. 加密备份与迁移(v2)
|
||||
@@ -75,8 +75,8 @@
|
||||
| `lease release <lease_token>` | 释放租约 |
|
||||
| `lease list [--active]` | 列出租约 |
|
||||
| `lease cleanup` | 清理过期租约 |
|
||||
| `account open <id>` / `open <id>` | 浏览器查看 |
|
||||
| `account reveal-profile <id>` | 打开 Profile 目录 |
|
||||
| `account open <id>` / `open <id>` | 打开浏览器 |
|
||||
| `account reveal-profile <id>` | 打开目录 |
|
||||
|
||||
### 兼容入口(旧版别名)
|
||||
|
||||
|
||||
@@ -162,7 +162,9 @@ def create_web_account_record(
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
shortcut_path = create_profile_browser_shortcut(resolved_profile_dir, safe_label, log)
|
||||
shortcut_path = create_profile_browser_shortcut(
|
||||
resolved_profile_dir, safe_label, log, url=resolved_url,
|
||||
)
|
||||
|
||||
log.info("account_add_web_success id=%s platform=%s profile_dir=%s", new_id, key, resolved_profile_dir)
|
||||
ok_payload = {
|
||||
|
||||
@@ -75,6 +75,35 @@ def resolve_chromium_channel():
|
||||
return "chrome"
|
||||
|
||||
|
||||
# 给人核对/手工登录用:跳过 Chrome 首次欢迎页并最大化。不要带 Playwright 的 --enable-automation。
|
||||
HUMAN_CHROME_FLAGS = (
|
||||
"--start-maximized",
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--disable-search-engine-choice-screen",
|
||||
)
|
||||
|
||||
|
||||
def human_chrome_launch_args(profile_dir, url=None):
|
||||
"""chrome.exe 之后的参数(Popen 列表形式,路径不额外加引号)。"""
|
||||
profile_abs = os.path.abspath(profile_dir)
|
||||
args = [f"--user-data-dir={profile_abs}", *HUMAN_CHROME_FLAGS]
|
||||
cleaned = (url or "").strip()
|
||||
if cleaned:
|
||||
args.append(cleaned)
|
||||
return args
|
||||
|
||||
|
||||
def human_chrome_shortcut_arguments(profile_dir, url=None):
|
||||
"""Windows .lnk Arguments 字符串(user-data-dir 带引号)。"""
|
||||
profile_abs = os.path.abspath(profile_dir)
|
||||
parts = [f'--user-data-dir="{profile_abs}"', *HUMAN_CHROME_FLAGS]
|
||||
cleaned = (url or "").strip()
|
||||
if cleaned:
|
||||
parts.append(cleaned)
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _print_browser_install_hint():
|
||||
print("❌ 未检测到 Google Chrome 或 Microsoft Edge,请先安装:")
|
||||
print(" • Chrome: https://www.google.com/chrome/")
|
||||
@@ -147,6 +176,10 @@ def cmd_reveal_profile(account_id):
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "result",
|
||||
"schema_version": 1,
|
||||
"status": "success",
|
||||
"message": "",
|
||||
"success": True,
|
||||
"data": {
|
||||
"account_id": int(target["id"]) if target.get("id") is not None else account_id,
|
||||
@@ -160,9 +193,9 @@ def cmd_reveal_profile(account_id):
|
||||
|
||||
def cmd_open(account_id):
|
||||
"""
|
||||
打开该账号的持久化浏览器,仅用于肉眼核对。
|
||||
打开该账号的持久化浏览器,仅用于肉眼核对 / 手工登录 / 抓 DOM。
|
||||
与 reveal-profile 同模式:本进程直接拉起系统 Chrome/Edge 后立即返回。
|
||||
不写数据库、不判定是否已登录、不起 Playwright / 额外 python 子进程。
|
||||
最大化、跳过 Chrome 首次欢迎页;不写数据库、不判定是否已登录、不起 Playwright。
|
||||
"""
|
||||
log = get_skill_logger()
|
||||
init_db()
|
||||
@@ -198,10 +231,8 @@ def cmd_open(account_id):
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# 与 profile_shortcut 一致:独立 user-data-dir 保存登录态
|
||||
cmd = [browser_exe, f"--user-data-dir={profile_abs}"]
|
||||
if url:
|
||||
cmd.append(url)
|
||||
# 与 profile_shortcut 同一套人用参数:独立 user-data-dir 保存登录态
|
||||
cmd = [browser_exe, *human_chrome_launch_args(profile_abs, url)]
|
||||
|
||||
log.info(
|
||||
"browser_open_launch exe=%s profile_dir=%s url=%s platform=%s",
|
||||
@@ -245,6 +276,10 @@ def cmd_open(account_id):
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "result",
|
||||
"schema_version": 1,
|
||||
"status": "success",
|
||||
"message": "",
|
||||
"success": True,
|
||||
"data": {
|
||||
"account_id": int(target["id"]) if target.get("id") is not None else account_id,
|
||||
|
||||
@@ -9,7 +9,10 @@ import subprocess
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from service.browser_service import resolve_chromium_executable
|
||||
from service.browser_service import (
|
||||
human_chrome_shortcut_arguments,
|
||||
resolve_chromium_executable,
|
||||
)
|
||||
|
||||
_WIN_LNK_FORBIDDEN = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
|
||||
|
||||
@@ -28,9 +31,10 @@ def create_profile_browser_shortcut(
|
||||
profile_dir: str,
|
||||
account_label: str,
|
||||
log: Optional[logging.Logger] = None,
|
||||
url: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
在 profile_dir 内创建 {account_label}.lnk,目标为系统 Chrome/Edge + --user-data-dir。
|
||||
在 profile_dir 内创建 {account_label}.lnk,与「打开浏览器」同一套人用参数。
|
||||
|
||||
非 Windows 或未安装 Chrome/Edge 时静默跳过,返回 None。
|
||||
成功返回 .lnk 绝对路径。
|
||||
@@ -52,7 +56,7 @@ def create_profile_browser_shortcut(
|
||||
|
||||
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}"'
|
||||
arguments = human_chrome_shortcut_arguments(profile_abs, url)
|
||||
working_dir = os.path.dirname(browser_exe)
|
||||
|
||||
ps = (
|
||||
|
||||
@@ -70,17 +70,49 @@ class TestCmdOpenDirectBrowser:
|
||||
payload = json.loads(out.splitlines()[-1])
|
||||
|
||||
assert payload["success"] is True
|
||||
assert payload["message"] == ""
|
||||
assert payload["type"] == "result"
|
||||
assert payload["status"] == "success"
|
||||
assert payload["data"]["pid"] == 5252
|
||||
assert payload["data"]["browser_exe"] == fake_exe
|
||||
assert spawned["cmd"][0] == fake_exe
|
||||
assert spawned["cmd"][1].startswith("--user-data-dir=")
|
||||
assert spawned["cmd"][2] == "https://example.com"
|
||||
assert "--start-maximized" in spawned["cmd"]
|
||||
assert "--no-first-run" in spawned["cmd"]
|
||||
assert "--no-default-browser-check" in spawned["cmd"]
|
||||
assert "--disable-search-engine-choice-screen" in spawned["cmd"]
|
||||
assert spawned["cmd"][-1] == "https://example.com"
|
||||
assert "--enable-automation" not in spawned["cmd"]
|
||||
# must NOT spawn python / open_child_runner
|
||||
joined = " ".join(spawned["cmd"])
|
||||
assert "open_child_runner" not in joined
|
||||
assert "python" not in joined.lower()
|
||||
|
||||
|
||||
class TestHumanChromeLaunchArgs:
|
||||
def test_flags_then_url(self, tmp_path):
|
||||
from service.browser_service import HUMAN_CHROME_FLAGS, human_chrome_launch_args
|
||||
|
||||
profile = str(tmp_path / "prof")
|
||||
args = human_chrome_launch_args(profile, "https://chat.deepseek.com")
|
||||
assert args[0].startswith("--user-data-dir=")
|
||||
for flag in HUMAN_CHROME_FLAGS:
|
||||
assert flag in args
|
||||
assert args[-1] == "https://chat.deepseek.com"
|
||||
assert "--enable-automation" not in args
|
||||
|
||||
def test_shortcut_arguments_quote_profile_and_include_url(self, tmp_path):
|
||||
from service.browser_service import human_chrome_shortcut_arguments
|
||||
|
||||
profile = tmp_path / "my prof"
|
||||
profile.mkdir()
|
||||
text = human_chrome_shortcut_arguments(str(profile), "https://example.com")
|
||||
assert '--user-data-dir="' in text
|
||||
assert "--start-maximized" in text
|
||||
assert "--no-first-run" in text
|
||||
assert text.endswith("https://example.com")
|
||||
|
||||
|
||||
class TestResolveChromiumChannel:
|
||||
"""resolve_chromium_channel returns a channel string when browsers exist."""
|
||||
|
||||
|
||||
@@ -31,6 +31,33 @@ class TestCreateProfileBrowserShortcut:
|
||||
out = create_profile_browser_shortcut(str(tmp_path), "demo")
|
||||
assert out is None
|
||||
|
||||
def test_windows_lnk_uses_human_chrome_args(self, tmp_path, monkeypatch) -> None:
|
||||
captured = {}
|
||||
|
||||
def fake_run(argv, **kwargs):
|
||||
captured["argv"] = list(argv)
|
||||
class Result:
|
||||
returncode = 0
|
||||
return Result()
|
||||
|
||||
monkeypatch.setattr("util.profile_shortcut.sys.platform", "win32")
|
||||
monkeypatch.setattr(
|
||||
"util.profile_shortcut.resolve_chromium_executable",
|
||||
lambda: r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||
)
|
||||
monkeypatch.setattr("util.profile_shortcut.subprocess.run", fake_run)
|
||||
monkeypatch.setattr("util.profile_shortcut.os.path.isfile", lambda path: True)
|
||||
|
||||
out = create_profile_browser_shortcut(
|
||||
str(tmp_path), "demo", url="https://chat.deepseek.com",
|
||||
)
|
||||
assert out is not None
|
||||
ps = captured["argv"][-1]
|
||||
assert "--start-maximized" in ps
|
||||
assert "--no-first-run" in ps
|
||||
assert "https://chat.deepseek.com" in ps
|
||||
assert "--enable-automation" not in ps
|
||||
|
||||
@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
|
||||
|
||||
@@ -54,6 +54,9 @@ def test_reveal_profile_opens_dir(clean_db, tmp_path, capsys):
|
||||
out = capsys.readouterr().out.strip()
|
||||
payload = json.loads(out.splitlines()[-1])
|
||||
assert payload["success"] is True
|
||||
assert payload["message"] == ""
|
||||
assert payload["type"] == "result"
|
||||
assert payload["status"] == "success"
|
||||
assert payload["data"]["account_id"] == aid
|
||||
assert payload["data"]["profile_dir"] == called_path
|
||||
|
||||
|
||||
50
tests/test_row_open_actions.py
Normal file
50
tests/test_row_open_actions.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据管理行内打开类:label 对齐模板,不与宿主 CRUD 重复。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
|
||||
def _skill_root() -> str:
|
||||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def _load_actions() -> list[dict]:
|
||||
path = os.path.join(_skill_root(), "assets", "actions.json")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
return list(manifest.get("actions") or [])
|
||||
|
||||
|
||||
def _by_id(actions: list[dict], action_id: str) -> dict:
|
||||
for item in actions:
|
||||
if item.get("id") == action_id:
|
||||
return item
|
||||
raise AssertionError(f"missing action {action_id}")
|
||||
|
||||
|
||||
class TestRowOpenActions(unittest.TestCase):
|
||||
def test_reveal_and_open_use_template_labels(self) -> None:
|
||||
actions = _load_actions()
|
||||
reveal = _by_id(actions, "reveal-profile")
|
||||
open_browser = _by_id(actions, "open-browser")
|
||||
self.assertEqual(reveal.get("label"), "打开目录")
|
||||
self.assertEqual(open_browser.get("label"), "打开浏览器")
|
||||
self.assertEqual(reveal.get("placements"), ["row"])
|
||||
self.assertEqual(open_browser.get("placements"), ["row"])
|
||||
self.assertEqual(reveal.get("interaction"), "direct")
|
||||
self.assertEqual(open_browser.get("interaction"), "direct")
|
||||
self.assertFalse(reveal.get("replaces"))
|
||||
self.assertFalse(open_browser.get("replaces"))
|
||||
|
||||
def test_create_takes_over_host_add_without_row_duplicate(self) -> None:
|
||||
create = _by_id(_load_actions(), "account-add-web")
|
||||
self.assertEqual(create.get("replaces"), "create")
|
||||
self.assertEqual(create.get("placements") or [], [])
|
||||
self.assertEqual(create.get("label"), "新增账号")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user