feat: simplify account create/edit and add row copy
All checks were successful
技能自动化发布 / release (push) Successful in 8s

Data-management add/edit now use short forms with enums; copy clones a platform account into a new empty profile.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-22 14:48:46 +08:00
parent 097dde0ebb
commit 9a20e7fc69
18 changed files with 935 additions and 66 deletions

View File

@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
"""account copy / update 与数据管理短表单语义。"""
from __future__ import annotations
import json
import os
from db.accounts_repo import find_accounts, get_account_by_id
def _add(clean_db, **kwargs):
from service.account_service import cmd_account_add_web
defaults = dict(
platform_input="douyin",
login_id="13800138000",
login_id_type="phone",
label="抖音 默认",
tenant_id="t1",
environment="production",
role="default",
provider_code=None,
url="https://www.douyin.com",
extra_json_str='{"hook":"keep"}',
profile_dir_override=None,
auth_strategy="qr_code_manual",
session_persistent=1,
device_fingerprint=None,
)
defaults.update(kwargs)
cmd_account_add_web(**defaults)
class TestAccountCopy:
def test_copy_clones_platform_not_login_or_profile(self, clean_db, capsys):
from service.account_service import cmd_account_copy
_add(clean_db)
source = find_accounts(platform_key="douyin")[0]
cmd_account_copy(source["id"])
out = capsys.readouterr().out.strip()
payload = json.loads(out.splitlines()[-1])
assert payload["success"] is True
assert payload["type"] == "result"
assert "已复制为" in payload["message"]
copy_id = payload["data"]["id"]
assert copy_id != source["id"]
copied = get_account_by_id(copy_id)
assert copied["platform_key"] == "douyin"
assert copied["account_label"] == "抖音 默认 副本"
assert copied["login_id"] == ""
assert copied["tenant_id"] == "t1"
assert copied["environment"] == "production"
assert copied["role"] == "default"
assert copied["url"] == "https://www.douyin.com"
assert copied["auth_strategy"] == "qr_code_manual"
assert copied["session_persistent"] == 1
assert copied["device_fingerprint"] is None
assert copied["status"] == "active"
assert copied["session_status"] == "unknown"
assert (copied.get("extra_json") or {}).get("hook") == "keep"
assert copied["profile_dir"]
assert copied["profile_dir"] != source["profile_dir"]
assert os.path.isdir(copied["profile_dir"])
def test_copy_second_increments_label(self, clean_db, capsys):
from service.account_service import cmd_account_copy
_add(clean_db)
source = find_accounts(platform_key="douyin")[0]
cmd_account_copy(source["id"])
capsys.readouterr()
cmd_account_copy(source["id"])
payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
assert payload["data"]["account_label"] == "抖音 默认 副本 2"
def test_copy_missing_id(self, clean_db, capsys):
from service.account_service import cmd_account_copy
cmd_account_copy(99999)
payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
assert payload["success"] is False
assert payload["error"]["code"] == "ERROR:ACCOUNT_NOT_FOUND"
class TestAccountUpdate:
def test_update_short_fields(self, clean_db, capsys):
from service.account_service import cmd_account_update
_add(clean_db)
source = find_accounts(platform_key="douyin")[0]
cmd_account_update(
source["id"],
label="抖音 备用",
login_id="backup@example.com",
login_id_type="unknown",
url="https://www.douyin.com/user",
status="disabled",
)
payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
assert payload["success"] is True
assert payload["message"] == "已保存"
updated = get_account_by_id(source["id"])
assert updated["account_label"] == "抖音 备用"
assert updated["login_id"] == "backup@example.com"
assert updated["login_id_type"] == "email"
assert updated["url"] == "https://www.douyin.com/user"
assert updated["status"] == "disabled"
assert updated["auth_strategy"] == "qr_code_manual"
assert updated["profile_dir"] == source["profile_dir"]
def test_update_empty_login_clears(self, clean_db, capsys):
from service.account_service import cmd_account_update
_add(clean_db)
source = find_accounts(platform_key="douyin")[0]
cmd_account_update(source["id"], login_id="", login_id_type="unknown")
capsys.readouterr()
updated = get_account_by_id(source["id"])
assert updated["login_id"] == ""
assert updated["login_id_type"] == "unknown"
assert updated["account_label"] == "抖音 默认"
def test_update_rejects_bad_status(self, clean_db, capsys):
from service.account_service import cmd_account_update
_add(clean_db)
source = find_accounts(platform_key="douyin")[0]
cmd_account_update(source["id"], status="nope")
payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
assert payload["success"] is False
class TestAddWebInfersLoginType:
def test_email_without_type(self, clean_db, capsys):
from service.account_service import cmd_account_add_web
cmd_account_add_web(
platform_input="deepseek",
login_id="a@b.com",
login_id_type=None,
label="ds",
tenant_id=None,
environment="production",
role="default",
provider_code=None,
url=None,
extra_json_str=None,
profile_dir_override=None,
)
capsys.readouterr()
acc = find_accounts(platform_key="deepseek")[0]
assert acc["login_id_type"] == "email"

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from util.account_identity import infer_login_id_type, next_copy_label, resolve_login_id_type
def test_infer_login_id_type():
assert infer_login_id_type(None) == "unknown"
assert infer_login_id_type("") == "unknown"
assert infer_login_id_type("demo@maersk.com") == "email"
assert infer_login_id_type("13800138000") == "phone"
assert infer_login_id_type("+8613800138000") == "phone"
assert infer_login_id_type("creator_01") == "username"
def test_resolve_login_id_type_keeps_explicit():
assert resolve_login_id_type("phone", "demo@x.com") == "phone"
assert resolve_login_id_type("unknown", "demo@x.com") == "email"
assert resolve_login_id_type(None, "13800138000") == "phone"
def test_next_copy_label_increments():
assert next_copy_label("抖音 默认", []) == "抖音 默认 副本"
assert next_copy_label("抖音 默认", ["抖音 默认 副本"]) == "抖音 默认 副本 2"
assert next_copy_label("抖音 默认 副本", ["抖音 默认 副本"]) == "抖音 默认 副本 2"
assert next_copy_label(
"抖音 默认",
["抖音 默认 副本", "抖音 默认 副本 2"],
) == "抖音 默认 副本 3"

View File

@@ -182,6 +182,62 @@ def test_profile_dir_not_editable(metadata_db):
assert int(cur.fetchone()[0]) == 0
def test_account_edit_form_columns_and_enums(metadata_db):
import json
cur = metadata_db.cursor()
expected_editable = {
"account_label",
"login_id",
"login_id_type",
"url",
"status",
}
readonly_runtime = {
"platform_key",
"tenant_id",
"environment",
"role",
"provider_code",
"session_status",
"auth_strategy",
"session_persistent",
"device_fingerprint",
}
for column_name in expected_editable:
cur.execute(
"""
SELECT editable FROM _jiangchang_columns
WHERE table_name = ? AND column_name = ?
""",
(ACCOUNTS_TABLE, column_name),
)
assert int(cur.fetchone()[0]) == 1, column_name
for column_name in readonly_runtime:
cur.execute(
"""
SELECT editable FROM _jiangchang_columns
WHERE table_name = ? AND column_name = ?
""",
(ACCOUNTS_TABLE, column_name),
)
assert int(cur.fetchone()[0]) == 0, column_name
cur.execute(
"""
SELECT options_json FROM _jiangchang_columns
WHERE table_name = ? AND column_name = 'status'
""",
(ACCOUNTS_TABLE,),
)
options = json.loads(cur.fetchone()[0])
values = {item["value"] for item in options}
labels = {item["label"] for item in options}
assert values == {"active", "disabled", "needs_review"}
assert "启用" in labels
assert "停用" in labels
def test_timestamp_columns_use_datetime_display_type(metadata_db):
cur = metadata_db.cursor()
for table_name in USER_BUSINESS_TABLES:

View File

@@ -44,6 +44,30 @@ class TestRowOpenActions(unittest.TestCase):
self.assertEqual(create.get("replaces"), "create")
self.assertEqual(create.get("placements") or [], [])
self.assertEqual(create.get("label"), "新增账号")
props = (create.get("inputSchema") or {}).get("properties") or {}
self.assertEqual(set(props), {"platform_key", "account_label", "login_id"})
def test_update_takes_over_host_edit(self) -> None:
update = _by_id(_load_actions(), "account-update")
self.assertEqual(update.get("replaces"), "update")
self.assertEqual(update.get("placements") or [], [])
self.assertEqual(update.get("label"), "编辑账号")
props = (update.get("inputSchema") or {}).get("properties") or {}
self.assertEqual(
set(props),
{"accountId", "account_label", "login_id", "login_id_type", "url", "status"},
)
self.assertIn("enum", props["login_id_type"])
self.assertIn("enum", props["status"])
def test_copy_is_direct_row_action(self) -> None:
copy = _by_id(_load_actions(), "account-copy")
self.assertEqual(copy.get("label"), "复制")
self.assertEqual(copy.get("placements"), ["row"])
self.assertEqual(copy.get("interaction"), "direct")
self.assertFalse(copy.get("replaces"))
steps = copy.get("steps") or []
self.assertTrue(any(s.get("do") == "reloadTable" for s in steps))
if __name__ == "__main__":

View File

@@ -27,6 +27,8 @@ def test_usage_includes_all_account_subcommands():
"init-master-key",
"export-credentials",
"import-credentials",
"copy",
"update",
"add-web",
"ensure-web",
"get-credential",