Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 718aeefd44 | |||
| 123195fcd9 | |||
| ba991a2462 | |||
| e9455cbb86 |
8
SKILL.md
8
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.17
|
version: 2.0.21
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
@@ -28,14 +28,14 @@ allowed-tools:
|
|||||||
## 何时使用本技能
|
## 何时使用本技能
|
||||||
|
|
||||||
1. **安装/升级后初始化库与数据管理元数据**:`init-db`(宿主会自动调用;也可手工执行)
|
1. **安装/升级后初始化库与数据管理元数据**:`init-db`(宿主会自动调用;也可手工执行)
|
||||||
2. **登记网页/RPA 账号**:`account add-web` — 平台 + 登录标识 + 自动生成 profile_dir;可选 `--auth-strategy` / `--session-persistent` / `--device-fingerprint`。
|
2. **登记网页/RPA 账号**:`account add-web` — 平台 + 登录标识 + 自动生成 profile_dir;可选 `--auth-strategy` / `--session-persistent` / `--device-fingerprint`。数据管理「新增」已绑定该能力(`replaces: create`),勿用宿主直写库新建账号。
|
||||||
3. **登记凭据(API Key / Token / 加密密码)**:`account add-secret` — `none` / `env` / `windows_credential` / `local_encrypted`。
|
3. **登记凭据(API Key / Token / 加密密码)**:`account add-secret` — `none` / `env` / `windows_credential` / `local_encrypted`。
|
||||||
4. **查看/列出账号**:`account get/list` — JSON 输出;list 可按 `--login-id` / `--label` 筛选。
|
4. **查看/列出账号**:`account get/list` — JSON 输出;list 可按 `--login-id` / `--label` 筛选。
|
||||||
5. **用人话定位账号**:`account resolve --login-id <手机号/用户名>` 或 `--label` / `--query`(不必先查数字 id)。
|
5. **用人话定位账号**:`account resolve --login-id <手机号/用户名>` 或 `--label` / `--query`(不必先查数字 id)。
|
||||||
6. **为 RPA 挑选账号**:`account pick-web` — 按平台/环境/角色,也可 `--login-id` / `--label`;业务首启推荐 `account ensure-web`(或 `pick-web --ensure`):无账号则自动 `add-web` 再返回。
|
6. **为 RPA 挑选账号**:`account pick-web` — 按平台/环境/角色,也可 `--login-id` / `--label`;业务首启推荐 `account ensure-web`(或 `pick-web --ensure`):无账号则自动 `add-web` 再返回。
|
||||||
7. **获取 API Key(凭据表)**:`credential pick` — 默认隐藏 secret,`--reveal` 时才返回。
|
7. **获取 API Key(凭据表)**:`credential pick` — 默认隐藏 secret,`--reveal` 时才返回。
|
||||||
8. **浏览器打开查看**:`account open <id>`(或兼容别名 `open <id>`)— **本进程**直接拉起系统 Chrome/Edge(`--user-data-dir`),与「打开文件位置」同模式;不做登录判定。数据管理行内按钮均为 `interaction: direct` + `sync`。
|
8. **打开浏览器**:`account open <id>`(或兼容别名 `open <id>`)— **本进程**直接拉起系统 Chrome/Edge(`--user-data-dir`),与「打开目录」同模式;不做登录判定。数据管理行内按钮均为 `interaction: direct` + `sync`,成功不弹提示。
|
||||||
9. **打开 Profile 目录**:`account reveal-profile <id>` — 在系统文件管理器中打开该账号 `profile_dir`。
|
9. **打开目录**:`account reveal-profile <id>` — 在系统文件管理器中打开该账号 `profile_dir`。
|
||||||
10. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。
|
10. **管理 lease**:`lease release/list/cleanup` — 防止 RPA 并发。
|
||||||
11. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。
|
11. **初始化加密**:`account init-master-key` — 生成 Fernet master.key。
|
||||||
12. **加密备份与迁移**:`account export-credentials --plaintext` / `account import-credentials` — 导出明文 JSON、在新环境重新加密写入。
|
12. **加密备份与迁移**:`account export-credentials --plaintext` / `account import-credentials` — 导出明文 JSON、在新环境重新加密写入。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# assets
|
# assets
|
||||||
|
|
||||||
- `actions.json`:匠厂宿主 Skill Action 清单(数据管理「账号信息」行内:打开文件位置 / 打开浏览器)。
|
- `actions.json`:匠厂宿主 Skill Action 清单(数据管理「账号信息」行内:打开目录 / 打开浏览器)。
|
||||||
- `examples/`:CLI 成功输出形状示例(虚构路径与数据)。
|
- `examples/`:CLI 成功输出形状示例(虚构路径与数据)。
|
||||||
- `schemas/`:`get` / `list-json` / `pick-web` 单条对象的轻量 JSON Schema(`additionalProperties: true` 允许扩展字段)。
|
- `schemas/`:`get` / `list-json` / `pick-web` 单条对象的轻量 JSON Schema(`additionalProperties: true` 允许扩展字段)。
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,83 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"skill": "account-manager",
|
"skill": "account-manager",
|
||||||
"actions": [
|
"actions": [
|
||||||
|
{
|
||||||
|
"id": "account-add-web",
|
||||||
|
"label": "新增账号",
|
||||||
|
"description": "登记网页/RPA 账号并自动创建浏览器 Profile 目录(走 account add-web,非宿主直写库)。",
|
||||||
|
"replaces": "create",
|
||||||
|
"placements": [],
|
||||||
|
"executionProfile": "sync",
|
||||||
|
"interaction": "form",
|
||||||
|
"bind": {
|
||||||
|
"tables": ["accounts"]
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["platform_key", "account_label"],
|
||||||
|
"properties": {
|
||||||
|
"platform_key": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "平台",
|
||||||
|
"description": "关联平台信息表;宿主按列 lookup 渲染下拉"
|
||||||
|
},
|
||||||
|
"account_label": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "账号名称"
|
||||||
|
},
|
||||||
|
"login_id": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "登录账号"
|
||||||
|
},
|
||||||
|
"login_id_type": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "登录标识类型",
|
||||||
|
"default": "unknown"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "租户编号"
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "环境",
|
||||||
|
"default": "production"
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "角色",
|
||||||
|
"default": "default"
|
||||||
|
},
|
||||||
|
"provider_code": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "供应商代码"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "入口链接"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entrypoint": {
|
||||||
|
"type": "cli",
|
||||||
|
"command": "account",
|
||||||
|
"args": [
|
||||||
|
"add-web",
|
||||||
|
"--platform", "{{platform_key}}",
|
||||||
|
"--label", "{{account_label}}",
|
||||||
|
"--login-id", "{{login_id}}",
|
||||||
|
"--login-id-type", "{{login_id_type}}",
|
||||||
|
"--tenant-id", "{{tenant_id}}",
|
||||||
|
"--environment", "{{environment}}",
|
||||||
|
"--role", "{{role}}",
|
||||||
|
"--provider-code", "{{provider_code}}",
|
||||||
|
"--url", "{{url}}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "reveal-profile",
|
"id": "reveal-profile",
|
||||||
"label": "打开文件位置",
|
"label": "打开目录",
|
||||||
"description": "在系统文件管理器中打开该账号的浏览器 Profile 目录。",
|
"description": "在系统文件管理器中打开该账号的浏览器 Profile 目录。",
|
||||||
"placements": ["row"],
|
"placements": ["row"],
|
||||||
"executionProfile": "sync",
|
"executionProfile": "sync",
|
||||||
@@ -34,7 +108,7 @@
|
|||||||
{
|
{
|
||||||
"id": "open-browser",
|
"id": "open-browser",
|
||||||
"label": "打开浏览器",
|
"label": "打开浏览器",
|
||||||
"description": "用该账号持久化 Profile 直接打开系统 Chrome/Edge,仅供肉眼核对(不做登录判定)。与打开文件位置同为一点即开。",
|
"description": "用该账号持久化 Profile 直接打开系统 Chrome/Edge,仅供肉眼核对(不做登录判定)。与打开目录同为一点即开。",
|
||||||
"placements": ["row"],
|
"placements": ["row"],
|
||||||
"executionProfile": "sync",
|
"executionProfile": "sync",
|
||||||
"interaction": "direct",
|
"interaction": "direct",
|
||||||
|
|||||||
@@ -75,8 +75,8 @@
|
|||||||
| `lease release <lease_token>` | 释放租约 |
|
| `lease release <lease_token>` | 释放租约 |
|
||||||
| `lease list [--active]` | 列出租约 |
|
| `lease list [--active]` | 列出租约 |
|
||||||
| `lease cleanup` | 清理过期租约 |
|
| `lease cleanup` | 清理过期租约 |
|
||||||
| `account open <id>` / `open <id>` | 浏览器查看 |
|
| `account open <id>` / `open <id>` | 打开浏览器 |
|
||||||
| `account reveal-profile <id>` | 打开 Profile 目录 |
|
| `account reveal-profile <id>` | 打开目录 |
|
||||||
|
|
||||||
### 兼容入口(旧版别名)
|
### 兼容入口(旧版别名)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ SQLite 无原生表/字段 COMMENT;中文展示名称通过这两张元数据
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
INTERNAL_METADATA_TABLES = frozenset({"_jiangchang_tables", "_jiangchang_columns"})
|
INTERNAL_METADATA_TABLES = frozenset({"_jiangchang_tables", "_jiangchang_columns"})
|
||||||
@@ -20,6 +21,26 @@ ACCOUNT_LEASES_TABLE = "account_leases"
|
|||||||
DATETIME_UNIX_SECONDS = "datetime_unix_seconds"
|
DATETIME_UNIX_SECONDS = "datetime_unix_seconds"
|
||||||
STANDARD_TIMESTAMP_COLUMNS = frozenset({"created_at", "updated_at"})
|
STANDARD_TIMESTAMP_COLUMNS = frozenset({"created_at", "updated_at"})
|
||||||
|
|
||||||
|
# Data-management lookup: host resolves options_json object into select options.
|
||||||
|
PLATFORM_KEY_LOOKUP_JSON = json.dumps(
|
||||||
|
{
|
||||||
|
"ref_table": "platforms",
|
||||||
|
"value_column": "platform_key",
|
||||||
|
"label_column": "display_name",
|
||||||
|
"filter_column": "enabled",
|
||||||
|
"filter_value": 1,
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
ACCOUNT_ID_LOOKUP_JSON = json.dumps(
|
||||||
|
{
|
||||||
|
"ref_table": "accounts",
|
||||||
|
"value_column": "id",
|
||||||
|
"label_column": "account_label",
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
|
||||||
USER_BUSINESS_TABLES = (
|
USER_BUSINESS_TABLES = (
|
||||||
PLATFORMS_TABLE,
|
PLATFORMS_TABLE,
|
||||||
ACCOUNTS_TABLE,
|
ACCOUNTS_TABLE,
|
||||||
@@ -123,7 +144,13 @@ ACCOUNTS_TABLE_METADATA = {
|
|||||||
|
|
||||||
ACCOUNTS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
ACCOUNTS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
||||||
_col("id", "编号", description="账号唯一编号", editable=0),
|
_col("id", "编号", description="账号唯一编号", editable=0),
|
||||||
_col("platform_key", "平台", description="所属平台键"),
|
_col(
|
||||||
|
"platform_key",
|
||||||
|
"平台",
|
||||||
|
description="所属平台键(来自平台信息表)",
|
||||||
|
display_type="lookup",
|
||||||
|
options_json=PLATFORM_KEY_LOOKUP_JSON,
|
||||||
|
),
|
||||||
_col("account_label", "账号名称", description="人可读的账号名称"),
|
_col("account_label", "账号名称", description="人可读的账号名称"),
|
||||||
_col("login_id", "登录账号", description="手机号、邮箱、用户名等登录标识"),
|
_col("login_id", "登录账号", description="手机号、邮箱、用户名等登录标识"),
|
||||||
_col("login_id_type", "登录标识类型", description="phone / email / username / unknown 等"),
|
_col("login_id_type", "登录标识类型", description="phone / email / username / unknown 等"),
|
||||||
@@ -158,8 +185,20 @@ CREDENTIALS_TABLE_METADATA = {
|
|||||||
|
|
||||||
CREDENTIALS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
CREDENTIALS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
||||||
_col("id", "编号", description="凭据唯一编号", editable=0),
|
_col("id", "编号", description="凭据唯一编号", editable=0),
|
||||||
_col("account_id", "账号编号", description="关联的 accounts.id,可空"),
|
_col(
|
||||||
_col("platform_key", "平台", description="所属平台键"),
|
"account_id",
|
||||||
|
"账号编号",
|
||||||
|
description="关联的 accounts.id,可空",
|
||||||
|
display_type="lookup",
|
||||||
|
options_json=ACCOUNT_ID_LOOKUP_JSON,
|
||||||
|
),
|
||||||
|
_col(
|
||||||
|
"platform_key",
|
||||||
|
"平台",
|
||||||
|
description="所属平台键(来自平台信息表)",
|
||||||
|
display_type="lookup",
|
||||||
|
options_json=PLATFORM_KEY_LOOKUP_JSON,
|
||||||
|
),
|
||||||
_col("credential_label", "凭据名称", description="人可读的凭据名称"),
|
_col("credential_label", "凭据名称", description="人可读的凭据名称"),
|
||||||
_col("credential_type", "凭据类型", description="api_key / password / bearer_token 等"),
|
_col("credential_type", "凭据类型", description="api_key / password / bearer_token 等"),
|
||||||
_col("secret_storage", "密钥存储方式", description="none / env / windows_credential / local_encrypted", editable=0),
|
_col("secret_storage", "密钥存储方式", description="none / env / windows_credential / local_encrypted", editable=0),
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ def cmd_reveal_profile(account_id):
|
|||||||
print(
|
print(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
|
"type": "result",
|
||||||
|
"schema_version": 1,
|
||||||
|
"status": "success",
|
||||||
|
"message": "",
|
||||||
"success": True,
|
"success": True,
|
||||||
"data": {
|
"data": {
|
||||||
"account_id": int(target["id"]) if target.get("id") is not None else account_id,
|
"account_id": int(target["id"]) if target.get("id") is not None else account_id,
|
||||||
@@ -245,6 +249,10 @@ def cmd_open(account_id):
|
|||||||
print(
|
print(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
|
"type": "result",
|
||||||
|
"schema_version": 1,
|
||||||
|
"status": "success",
|
||||||
|
"message": "",
|
||||||
"success": True,
|
"success": True,
|
||||||
"data": {
|
"data": {
|
||||||
"account_id": int(target["id"]) if target.get("id") is not None else account_id,
|
"account_id": int(target["id"]) if target.get("id") is not None else account_id,
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ class TestCmdOpenDirectBrowser:
|
|||||||
payload = json.loads(out.splitlines()[-1])
|
payload = json.loads(out.splitlines()[-1])
|
||||||
|
|
||||||
assert payload["success"] is True
|
assert payload["success"] is True
|
||||||
|
assert payload["message"] == ""
|
||||||
|
assert payload["type"] == "result"
|
||||||
|
assert payload["status"] == "success"
|
||||||
assert payload["data"]["pid"] == 5252
|
assert payload["data"]["pid"] == 5252
|
||||||
assert payload["data"]["browser_exe"] == fake_exe
|
assert payload["data"]["browser_exe"] == fake_exe
|
||||||
assert spawned["cmd"][0] == fake_exe
|
assert spawned["cmd"][0] == fake_exe
|
||||||
|
|||||||
@@ -102,6 +102,44 @@ def test_sensitive_credential_fields_not_editable(metadata_db):
|
|||||||
assert int(searchable) == 0, column_name
|
assert int(searchable) == 0, column_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_account_and_credential_platform_lookups(metadata_db):
|
||||||
|
import json
|
||||||
|
|
||||||
|
cur = metadata_db.cursor()
|
||||||
|
for table_name, column_name in (
|
||||||
|
(ACCOUNTS_TABLE, "platform_key"),
|
||||||
|
(CREDENTIALS_TABLE, "platform_key"),
|
||||||
|
):
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT display_type, options_json
|
||||||
|
FROM _jiangchang_columns
|
||||||
|
WHERE table_name = ? AND column_name = ?
|
||||||
|
""",
|
||||||
|
(table_name, column_name),
|
||||||
|
)
|
||||||
|
display_type, options_json = cur.fetchone()
|
||||||
|
assert display_type == "lookup", table_name
|
||||||
|
payload = json.loads(options_json)
|
||||||
|
assert payload["ref_table"] == "platforms"
|
||||||
|
assert payload["value_column"] == "platform_key"
|
||||||
|
assert payload["label_column"] == "display_name"
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT display_type, options_json
|
||||||
|
FROM _jiangchang_columns
|
||||||
|
WHERE table_name = ? AND column_name = 'account_id'
|
||||||
|
""",
|
||||||
|
(CREDENTIALS_TABLE,),
|
||||||
|
)
|
||||||
|
display_type, options_json = cur.fetchone()
|
||||||
|
assert display_type == "lookup"
|
||||||
|
payload = json.loads(options_json)
|
||||||
|
assert payload["ref_table"] == "accounts"
|
||||||
|
assert payload["value_column"] == "id"
|
||||||
|
|
||||||
|
|
||||||
def test_lease_token_not_editable(metadata_db):
|
def test_lease_token_not_editable(metadata_db):
|
||||||
cur = metadata_db.cursor()
|
cur = metadata_db.cursor()
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ def test_reveal_profile_opens_dir(clean_db, tmp_path, capsys):
|
|||||||
out = capsys.readouterr().out.strip()
|
out = capsys.readouterr().out.strip()
|
||||||
payload = json.loads(out.splitlines()[-1])
|
payload = json.loads(out.splitlines()[-1])
|
||||||
assert payload["success"] is True
|
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"]["account_id"] == aid
|
||||||
assert payload["data"]["profile_dir"] == called_path
|
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