From 014cda7144e9fa09ee993d18822018c059ce1632 Mon Sep 17 00:00:00 2001 From: wanghaomin Date: Tue, 11 Aug 2026 06:56:06 +0000 Subject: [PATCH] feat(base): add button rule commands --- shortcuts/base/base_dryrun_ops_test.go | 41 ++++ shortcuts/base/base_shortcuts_test.go | 1 + shortcuts/base/button_rule.go | 178 ++++++++++++++++++ shortcuts/base/helpers.go | 2 + shortcuts/base/shortcuts.go | 3 + skills/lark-base/SKILL.md | 1 + .../references/lark-base-field-json.md | 19 +- 7 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 shortcuts/base/button_rule.go diff --git a/shortcuts/base/base_dryrun_ops_test.go b/shortcuts/base/base_dryrun_ops_test.go index 234ac84f9b..08ff78e398 100644 --- a/shortcuts/base/base_dryrun_ops_test.go +++ b/shortcuts/base/base_dryrun_ops_test.go @@ -119,6 +119,47 @@ func TestDryRunFieldOps(t *testing.T) { } } +func TestDryRunButtonRuleOps(t *testing.T) { + ctx := context.Background() + rt := newBaseTestRuntime( + map[string]string{ + "base-token": "app_x", + "table-id": "tbl_1", + "field-id": "fld_button", + "workflow-id": "wkf_1", + }, + nil, + nil, + ) + + assertDryRunContains(t, dryRunButtonBind(ctx, rt), "PUT /open-apis/base/v3/bases/app_x/tables/tbl_1/fields/fld_button/button_rule", `"workflow_id":"wkf_1"`) + assertDryRunContains(t, dryRunButtonGet(ctx, rt), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/fields/fld_button/button_rule") + assertDryRunContains(t, dryRunButtonUnbind(ctx, rt), "DELETE /open-apis/base/v3/bases/app_x/tables/tbl_1/fields/fld_button/button_rule") +} + +func TestValidateButtonRuleOps(t *testing.T) { + valid := newBaseTestRuntime(map[string]string{ + "base-token": "app_x", "table-id": "tbl_1", "field-id": "fld_button", "workflow-id": "wkfAbcdefg", + }, nil, nil) + if err := validateButtonBind(valid); err != nil { + t.Fatalf("valid button bind rejected: %v", err) + } + + badWorkflow := newBaseTestRuntime(map[string]string{ + "base-token": "app_x", "table-id": "tbl_1", "field-id": "fld_button", "workflow-id": "tblAbcdefg", + }, nil, nil) + if err := validateButtonBind(badWorkflow); err == nil || !strings.Contains(err.Error(), "wkf prefix") { + t.Fatalf("expected public workflow ID validation error, got %v", err) + } + + missingField := newBaseTestRuntime(map[string]string{ + "base-token": "app_x", "table-id": "tbl_1", "workflow-id": "wkfAbcdefg", + }, nil, nil) + if err := validateButtonRuleLocator(missingField); err == nil || !strings.Contains(err.Error(), "--field-id") { + t.Fatalf("expected missing field validation error, got %v", err) + } +} + func TestDryRunRecordOps(t *testing.T) { ctx := context.Background() diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index 96de23a4a6..b5e21acecf 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -169,6 +169,7 @@ func TestShortcutsCatalog(t *testing.T) { "+base-get", "+base-copy", "+base-create", "+role-create", "+role-delete", "+role-update", "+role-list", "+role-get", "+advperm-enable", "+advperm-disable", "+workflow-list", "+workflow-get", "+workflow-create", "+workflow-update", "+workflow-enable", "+workflow-disable", + "+button-bind", "+button-get", "+button-unbind", "+data-query", "+form-create", "+form-delete", "+form-list", "+form-update", "+form-get", "+form-detail", "+form-questions-create", "+form-questions-delete", "+form-questions-update", "+form-questions-list", diff --git a/shortcuts/base/button_rule.go b/shortcuts/base/button_rule.go new file mode 100644 index 0000000000..b6adf375a9 --- /dev/null +++ b/shortcuts/base/button_rule.go @@ -0,0 +1,178 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + "strings" + + "github.com/larksuite/cli/shortcuts/common" +) + +var BaseButtonBind = common.Shortcut{ + Service: "base", + Command: "+button-bind", + Description: "Bind a button field to a workflow", + Risk: "write", + Scopes: []string{"base:field:update", "base:workflow:update"}, + AuthTypes: authTypes(), + Flags: []common.Flag{ + baseTokenFlag(true), + tableRefFlag(true), + fieldRefFlag(true), + {Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true}, + }, + Tips: []string{ + "Create the button-trigger workflow first, then create the button field, then bind them with this command.", + "Button field JSON must not include workflow_id; binding is managed only through button_rule APIs.", + "workflow-id must start with wkf; do not pass a tbl table ID or raw internal automation ID.", + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + return validateButtonBind(runtime) + }, + DryRun: dryRunButtonBind, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + return executeButtonBind(runtime) + }, +} + +var BaseButtonGet = common.Shortcut{ + Service: "base", + Command: "+button-get", + Description: "Get the workflow bound to a button field", + Risk: "read", + Scopes: []string{"base:field:read", "base:workflow:read"}, + AuthTypes: authTypes(), + Flags: []common.Flag{ + baseTokenFlag(true), + tableRefFlag(true), + fieldRefFlag(true), + }, + Tips: []string{ + "Returns the button_rule binding for a button field; use +workflow-get for workflow details.", + "The binding workflow_id is the public wkf-prefixed workflow ID.", + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + return validateButtonRuleLocator(runtime) + }, + DryRun: dryRunButtonGet, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + return executeButtonGet(runtime) + }, +} + +var BaseButtonUnbind = common.Shortcut{ + Service: "base", + Command: "+button-unbind", + Description: "Unbind a button field from its workflow", + Risk: "high-risk-write", + Scopes: []string{"base:field:update", "base:workflow:update"}, + AuthTypes: authTypes(), + Flags: []common.Flag{ + baseTokenFlag(true), + tableRefFlag(true), + fieldRefFlag(true), + }, + Tips: []string{ + "Unbind only removes the button_rule relation; it does not delete the button field or workflow.", + "Agent guidance: for high-risk writes, explain the exact target and pass --yes without asking again when the user has already asked you to perform this action.", + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + return validateButtonRuleLocator(runtime) + }, + DryRun: dryRunButtonUnbind, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + return executeButtonUnbind(runtime) + }, +} + +func validateButtonRuleLocator(runtime *common.RuntimeContext) error { + if strings.TrimSpace(runtime.Str("base-token")) == "" { + return baseFlagErrorf("--base-token must not be blank") + } + if strings.TrimSpace(baseTableID(runtime)) == "" { + return baseFlagErrorf("--table-id must not be blank") + } + if strings.TrimSpace(runtime.Str("field-id")) == "" { + return baseFlagErrorf("--field-id must not be blank") + } + return nil +} + +func validateButtonBind(runtime *common.RuntimeContext) error { + if err := validateButtonRuleLocator(runtime); err != nil { + return err + } + workflowID := strings.TrimSpace(runtime.Str("workflow-id")) + if workflowID == "" { + return baseFlagErrorf("--workflow-id must not be blank") + } + if !strings.HasPrefix(workflowID, "wkf") { + return baseFlagErrorf("--workflow-id must be a public workflow ID with wkf prefix") + } + return nil +} + +func buttonRulePath(runtime *common.RuntimeContext) string { + return baseV3Path( + "bases", runtime.Str("base-token"), + "tables", baseTableID(runtime), + "fields", runtime.Str("field-id"), + "button_rule", + ) +} + +func dryRunButtonBind(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + body := map[string]interface{}{"workflow_id": runtime.Str("workflow-id")} + return common.NewDryRunAPI(). + PUT("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/button_rule"). + Body(body). + Set("base_token", runtime.Str("base-token")). + Set("table_id", baseTableID(runtime)). + Set("field_id", runtime.Str("field-id")) +} + +func dryRunButtonGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + return common.NewDryRunAPI(). + GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/button_rule"). + Set("base_token", runtime.Str("base-token")). + Set("table_id", baseTableID(runtime)). + Set("field_id", runtime.Str("field-id")) +} + +func dryRunButtonUnbind(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + return common.NewDryRunAPI(). + DELETE("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/button_rule"). + Set("base_token", runtime.Str("base-token")). + Set("table_id", baseTableID(runtime)). + Set("field_id", runtime.Str("field-id")) +} + +func executeButtonBind(runtime *common.RuntimeContext) error { + body := map[string]interface{}{"workflow_id": runtime.Str("workflow-id")} + data, err := baseV3CallAny(runtime, "PUT", buttonRulePath(runtime), nil, body) + if err != nil { + return err + } + runtime.Out(map[string]interface{}{"button_rule": data, "bound": true}, nil) + return nil +} + +func executeButtonGet(runtime *common.RuntimeContext) error { + data, err := baseV3CallAny(runtime, "GET", buttonRulePath(runtime), nil, nil) + if err != nil { + return err + } + runtime.Out(map[string]interface{}{"button_rule": data}, nil) + return nil +} + +func executeButtonUnbind(runtime *common.RuntimeContext) error { + data, err := baseV3CallAny(runtime, "DELETE", buttonRulePath(runtime), nil, nil) + if err != nil { + return err + } + runtime.Out(map[string]interface{}{"button_rule": data, "unbound": true}, nil) + return nil +} diff --git a/shortcuts/base/helpers.go b/shortcuts/base/helpers.go index f726891b4b..39e27b3495 100644 --- a/shortcuts/base/helpers.go +++ b/shortcuts/base/helpers.go @@ -175,6 +175,8 @@ func resolveFieldTypeSpec(typeName string) (fieldTypeSpec, error) { return fieldTypeSpec{Type: "datetime", Extra: map[string]interface{}{"style": map[string]interface{}{"format": "yyyy/MM/dd"}}}, nil case "checkbox": return fieldTypeSpec{Type: "checkbox"}, nil + case "button", "buttonfield", "button_field", "button-field": + return fieldTypeSpec{Type: "button"}, nil case "user", "groupchat", "group_chat", "group-chat": return fieldTypeSpec{Type: "user", Extra: map[string]interface{}{"multiple": true}}, nil case "attachment": diff --git a/shortcuts/base/shortcuts.go b/shortcuts/base/shortcuts.go index 3c9a7403c7..2c33ccc97e 100644 --- a/shortcuts/base/shortcuts.go +++ b/shortcuts/base/shortcuts.go @@ -73,6 +73,9 @@ func Shortcuts() []common.Shortcut { BaseWorkflowUpdate, BaseWorkflowEnable, BaseWorkflowDisable, + BaseButtonBind, + BaseButtonGet, + BaseButtonUnbind, BaseDataQuery, BaseFormCreate, BaseFormDelete, diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index e040bab83b..6dba0ee3be 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -72,6 +72,7 @@ metadata: | 分享表单详情 | `+form-detail --share-token ` | 只接受表单分享链接里的 `share_token`,不要传 `--base-token` / `--form-id`;提交前读 [lark-base-form-detail.md](references/lark-base-form-detail.md) | | 仪表盘与组件 | `+dashboard-*` / `+dashboard-block-*` | 提到图表/看板/block 时先读 [lark-base-dashboard.md](references/lark-base-dashboard.md);组件 `data_config` 读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md);读取一个或多个图表计算结果用 `+dashboard-block-get-data`;读取完整仪表盘时按 block 类型分流,文本和不支持直接取数的图表按 reference 恢复 | | Workflow | `+workflow-*` | 创建/更新或理解 steps 时读入口 [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md) 和 steps JSON SSOT [lark-base-workflow-schema.md](references/lark-base-workflow-schema.md);list/get/enable/disable 只处理 workflow ID 与启停状态 | +| 按钮字段绑定 Workflow | `+button-bind/get/unbind` | 先创建带按钮触发器的 workflow,再创建 `type:"button"` 字段,最后用 `+button-bind` 绑定 `wkf_` workflow ID;字段 JSON 不写 `workflow_id` | | 高级权限与角色 | `+advperm-*` / `+role-*` | 角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md);角色 create/update 或解读完整配置再读权限 JSON SSOT [role-config.md](references/role-config.md);系统角色不可删除;关闭高级权限会影响自定义角色 | ## Base 心智模型 diff --git a/skills/lark-base/references/lark-base-field-json.md b/skills/lark-base/references/lark-base-field-json.md index 5667ca0bf4..f6fe1a86b3 100644 --- a/skills/lark-base/references/lark-base-field-json.md +++ b/skills/lark-base/references/lark-base-field-json.md @@ -41,6 +41,7 @@ | `lookup` | `type` `name` `from` `select` `where` | `aggregate` | | `auto_number` | `type` `name` | `style.rules` | | `attachment` / `location` / `checkbox` | `type` `name` | 无 | +| `button` | `type` `name` | 无;工作流绑定另走 `+button-bind` | 所有类型都可额外传 `description`;上表的“常见补充字段”只列类型特有配置。 @@ -510,6 +511,22 @@ { "type": "checkbox", "name": "完成" } ``` +### 3.13 button + +按钮字段只定义字段本身,不在字段 JSON 里写 `workflow_id`。 + +```json +{ "type": "button", "name": "同步线索" } +``` + +完整工作流是三步: + +1. 用 `+workflow-create` 创建 `ButtonTrigger` 工作流,拿到 `wkf...`。 +2. 用 `+field-create` 创建 `type:"button"` 字段,拿到 `fld...`。 +3. 用 `+button-bind --base-token --table-id --field-id --workflow-id ` 建立绑定。 + +查询和解除绑定分别用 `+button-get`、`+button-unbind`。`+field-update` 不负责绑定或解绑工作流。 + ## 4. 创建与更新 - `+field-create`:按目标字段配置直接构造 `--json`。 @@ -517,7 +534,7 @@ ## 5. 暂不支持字段 -Object(对象字段)、Button(按钮字段)、Stage(流程字段)暂时都没有被 CLI 支持。这些字段会展示为 `not_support` 字段并被保护:不允许修改,不允许读取内容。 +Object(对象字段)、Stage(流程字段)暂时没有被 CLI 支持。这些字段会展示为 `not_support` 字段并被保护:不允许修改,不允许读取内容。 遇到暂不支持的字段类型时,直接说明 Base CLI 当前不支持并停止;不要猜测未注册的字段 JSON、service 或 schema,也不要用其他字段类型冒充目标能力。