From 6f5dc5b4263e39eb45f2e21ff86291ffd366b268 Mon Sep 17 00:00:00 2001 From: wanghaomin Date: Mon, 10 Aug 2026 09:12:17 +0000 Subject: [PATCH] feat(base): support button field workflow binding Add base shortcuts for button field workflow bind, query, unbind, and reverse lookup by workflow. Document the workflow-first button field flow and cover dry-run/execute behavior. Co-authored-by: TRAE CLI --- shortcuts/base/base_execute_test.go | 90 +++++++++++++++ shortcuts/base/base_shortcuts_test.go | 4 +- shortcuts/base/field_button_bind.go | 39 +++++++ shortcuts/base/field_button_binding_get.go | 29 +++++ shortcuts/base/field_button_ops.go | 106 ++++++++++++++++++ shortcuts/base/field_button_unbind.go | 29 +++++ shortcuts/base/shortcuts.go | 4 + shortcuts/base/workflow_button_fields.go | 35 ++++++ skills/lark-base/SKILL.md | 2 + .../references/lark-base-field-create.md | 9 ++ .../references/lark-base-field-json.md | 31 ++++- tests/cli_e2e/base/base_field_dryrun_test.go | 78 +++++++++++++ 12 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 shortcuts/base/field_button_bind.go create mode 100644 shortcuts/base/field_button_binding_get.go create mode 100644 shortcuts/base/field_button_ops.go create mode 100644 shortcuts/base/field_button_unbind.go create mode 100644 shortcuts/base/workflow_button_fields.go diff --git a/shortcuts/base/base_execute_test.go b/shortcuts/base/base_execute_test.go index 371400729a..b13f676acd 100644 --- a/shortcuts/base/base_execute_test.go +++ b/shortcuts/base/base_execute_test.go @@ -4045,3 +4045,93 @@ func TestBaseViewExecutePropertyGettersAndExtendedSetters(t *testing.T) { } }) } + +func TestBaseFieldButtonWorkflowExecute(t *testing.T) { + t.Run("bind sends wkf workflow id", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_x/button-workflow:bind", + BodyFilter: func(body []byte) bool { + return string(body) == `{"workflow_id":"wkf_x"}` + }, + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{"workflow_id": "wkf_x", "table_id": "tbl_x", "field_id": "fld_x"}, + }, + }) + err := runShortcut(t, BaseFieldButtonBind, []string{"+field-button-bind", "--base-token", "app_x", "--table-id", "tbl_x", "--field-id", "fld_x", "--workflow-id", "wkf_x"}, factory, stdout) + if err != nil { + t.Fatalf("err=%v", err) + } + got := stdout.String() + for _, want := range []string{`"workflow_id": "wkf_x"`, `"table_id": "tbl_x"`, `"field_id": "fld_x"`} { + if !strings.Contains(got, want) { + t.Fatalf("stdout missing %q:\n%s", want, got) + } + } + }) + + t.Run("bind rejects internal numeric workflow id", func(t *testing.T) { + factory, stdout, _ := newExecuteFactory(t) + err := runShortcut(t, BaseFieldButtonBind, []string{"+field-button-bind", "--base-token", "app_x", "--table-id", "tbl_x", "--field-id", "fld_x", "--workflow-id", "123"}, factory, stdout) + if err == nil || !strings.Contains(err.Error(), "wkf prefix") { + t.Fatalf("err=%v", err) + } + }) + + t.Run("get field binding", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_x/button-workflow", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{"workflow_id": "wkf_x"}, + }, + }) + err := runShortcut(t, BaseFieldButtonBindingGet, []string{"+field-button-binding-get", "--base-token", "app_x", "--table-id", "tbl_x", "--field-id", "fld_x"}, factory, stdout) + if err != nil { + t.Fatalf("err=%v", err) + } + if got := stdout.String(); !strings.Contains(got, `"workflow_id": "wkf_x"`) { + t.Fatalf("stdout=%s", got) + } + }) + + t.Run("unbind", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_x/button-workflow:unbind", + Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"unbound": true}}, + }) + err := runShortcut(t, BaseFieldButtonUnbind, []string{"+field-button-unbind", "--base-token", "app_x", "--table-id", "tbl_x", "--field-id", "fld_x"}, factory, stdout) + if err != nil { + t.Fatalf("err=%v", err) + } + if got := stdout.String(); !strings.Contains(got, `"unbound": true`) { + t.Fatalf("stdout=%s", got) + } + }) + + t.Run("workflow button fields", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/base/v3/bases/app_x/workflows/wkf_x/button-fields", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{"items": []interface{}{map[string]interface{}{"table_id": "tbl_x", "field_id": "fld_x"}}}, + }, + }) + err := runShortcut(t, BaseWorkflowButtonFields, []string{"+workflow-button-fields", "--base-token", "app_x", "--workflow-id", "wkf_x"}, factory, stdout) + if err != nil { + t.Fatalf("err=%v", err) + } + got := stdout.String() + if !strings.Contains(got, `"table_id": "tbl_x"`) || !strings.Contains(got, `"field_id": "fld_x"`) { + t.Fatalf("stdout=%s", got) + } + }) +} diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index 96de23a4a6..ec5919d791 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -162,13 +162,13 @@ func TestShortcutsCatalog(t *testing.T) { "+url-resolve", "+title-resolve", "+base-block-list", "+base-block-create", "+base-block-move", "+base-block-rename", "+base-block-delete", "+table-list", "+table-get", "+table-create", "+table-update", "+table-delete", "+table-copy", "+table-copy-status", - "+field-list", "+field-get", "+field-create", "+field-update", "+field-delete", "+field-search-options", + "+field-list", "+field-get", "+field-create", "+field-update", "+field-delete", "+field-search-options", "+field-button-bind", "+field-button-binding-get", "+field-button-unbind", "+view-list", "+view-get", "+view-create", "+view-delete", "+view-get-filter", "+view-set-filter", "+view-get-visible-fields", "+view-set-visible-fields", "+view-get-group", "+view-set-group", "+view-get-sort", "+view-set-sort", "+view-get-timebar", "+view-set-timebar", "+view-get-card", "+view-set-card", "+view-rename", "+record-list", "+record-search", "+record-get", "+record-upsert", "+record-batch-create", "+record-batch-update", "+record-share-link-create", "+record-upload-attachment", "+record-download-attachment", "+record-remove-attachment", "+record-delete", "+record-history-list", "+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", + "+workflow-list", "+workflow-get", "+workflow-create", "+workflow-update", "+workflow-enable", "+workflow-disable", "+workflow-button-fields", "+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/field_button_bind.go b/shortcuts/base/field_button_bind.go new file mode 100644 index 0000000000..4658a7d4a5 --- /dev/null +++ b/shortcuts/base/field_button_bind.go @@ -0,0 +1,39 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + + "github.com/larksuite/cli/shortcuts/common" +) + +var BaseFieldButtonBind = common.Shortcut{ + Service: "base", + Command: "+field-button-bind", + Description: "Bind or rebind 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 returned by +workflow-create or +workflow-list (wkf... prefix)", Required: true}, + }, + Tips: []string{ + `Example: lark-cli base +field-button-bind --base-token --table-id --field-id --workflow-id `, + "Create the workflow first with +workflow-create; new workflows are disabled until +workflow-enable is called.", + "Create the button field without a workflow ID in its property, then bind it with this command.", + "workflow-id must be the wkf... OpenAPI ID. Internal numeric workflow IDs are not accepted or displayed.", + "Binding is the source of truth; do not use property.trigger.config.id to create or update a binding.", + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + return validateWorkflowIDFlag(runtime) + }, + DryRun: dryRunFieldButtonBind, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + return executeFieldButtonBind(runtime) + }, +} diff --git a/shortcuts/base/field_button_binding_get.go b/shortcuts/base/field_button_binding_get.go new file mode 100644 index 0000000000..e5110009f3 --- /dev/null +++ b/shortcuts/base/field_button_binding_get.go @@ -0,0 +1,29 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + + "github.com/larksuite/cli/shortcuts/common" +) + +var BaseFieldButtonBindingGet = common.Shortcut{ + Service: "base", + Command: "+field-button-binding-get", + Description: "Get the workflow binding for 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{ + `Example: lark-cli base +field-button-binding-get --base-token --table-id --field-id `, + "Returns the workflow_id as a wkf... OpenAPI ID when a binding exists.", + "The binding source of truth is the button workflow relation, not field property.trigger.", + }, + DryRun: dryRunFieldButtonBindingGet, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + return executeFieldButtonBindingGet(runtime) + }, +} diff --git a/shortcuts/base/field_button_ops.go b/shortcuts/base/field_button_ops.go new file mode 100644 index 0000000000..ef9ac8838d --- /dev/null +++ b/shortcuts/base/field_button_ops.go @@ -0,0 +1,106 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + "strings" + + "github.com/larksuite/cli/shortcuts/common" +) + +func validateWorkflowIDFlag(runtime *common.RuntimeContext) error { + 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 an OpenAPI workflow ID with wkf prefix; internal numeric workflow IDs are not accepted") + } + return nil +} + +func dryRunFieldButtonBind(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + return common.NewDryRunAPI(). + POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/button-workflow:bind"). + Body(map[string]interface{}{"workflow_id": runtime.Str("workflow-id")}). + Set("base_token", runtime.Str("base-token")). + Set("table_id", baseTableID(runtime)). + Set("field_id", runtime.Str("field-id")) +} + +func dryRunFieldButtonBindingGet(_ 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-workflow"). + Set("base_token", runtime.Str("base-token")). + Set("table_id", baseTableID(runtime)). + Set("field_id", runtime.Str("field-id")) +} + +func dryRunFieldButtonUnbind(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + return common.NewDryRunAPI(). + POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/button-workflow:unbind"). + Set("base_token", runtime.Str("base-token")). + Set("table_id", baseTableID(runtime)). + Set("field_id", runtime.Str("field-id")) +} + +func dryRunWorkflowButtonFields(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + return common.NewDryRunAPI(). + GET("/open-apis/base/v3/bases/:base_token/workflows/:workflow_id/button-fields"). + Set("base_token", runtime.Str("base-token")). + Set("workflow_id", runtime.Str("workflow-id")) +} + +func executeFieldButtonBind(runtime *common.RuntimeContext) error { + data, err := baseV3Call(runtime, "POST", + baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "fields", runtime.Str("field-id"), "button-workflow:bind"), + nil, + map[string]interface{}{"workflow_id": runtime.Str("workflow-id")}, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil +} + +func executeFieldButtonBindingGet(runtime *common.RuntimeContext) error { + data, err := baseV3Call(runtime, "GET", + baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "fields", runtime.Str("field-id"), "button-workflow"), + nil, + nil, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil +} + +func executeFieldButtonUnbind(runtime *common.RuntimeContext) error { + data, err := baseV3Call(runtime, "POST", + baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "fields", runtime.Str("field-id"), "button-workflow:unbind"), + nil, + nil, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil +} + +func executeWorkflowButtonFields(runtime *common.RuntimeContext) error { + data, err := baseV3Call(runtime, "GET", + baseV3Path("bases", runtime.Str("base-token"), "workflows", runtime.Str("workflow-id"), "button-fields"), + nil, + nil, + ) + if err != nil { + return err + } + runtime.Out(data, nil) + return nil +} diff --git a/shortcuts/base/field_button_unbind.go b/shortcuts/base/field_button_unbind.go new file mode 100644 index 0000000000..3c8a1c39ec --- /dev/null +++ b/shortcuts/base/field_button_unbind.go @@ -0,0 +1,29 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + + "github.com/larksuite/cli/shortcuts/common" +) + +var BaseFieldButtonUnbind = common.Shortcut{ + Service: "base", + Command: "+field-button-unbind", + Description: "Unbind a button field from its workflow", + Risk: "write", + Scopes: []string{"base:field:update", "base:workflow:update"}, + AuthTypes: authTypes(), + Flags: []common.Flag{baseTokenFlag(true), tableRefFlag(true), fieldRefFlag(true)}, + Tips: []string{ + `Example: lark-cli base +field-button-unbind --base-token --table-id --field-id `, + "Unbind is idempotent; repeating it should still return success when the server has no binding to delete.", + "Unbinding does not delete the field and does not disable or delete the workflow.", + }, + DryRun: dryRunFieldButtonUnbind, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + return executeFieldButtonUnbind(runtime) + }, +} diff --git a/shortcuts/base/shortcuts.go b/shortcuts/base/shortcuts.go index 3c9a7403c7..de299279cb 100644 --- a/shortcuts/base/shortcuts.go +++ b/shortcuts/base/shortcuts.go @@ -28,6 +28,9 @@ func Shortcuts() []common.Shortcut { BaseFieldUpdate, BaseFieldDelete, BaseFieldSearchOptions, + BaseFieldButtonBind, + BaseFieldButtonBindingGet, + BaseFieldButtonUnbind, BaseViewList, BaseViewGet, BaseViewCreate, @@ -73,6 +76,7 @@ func Shortcuts() []common.Shortcut { BaseWorkflowUpdate, BaseWorkflowEnable, BaseWorkflowDisable, + BaseWorkflowButtonFields, BaseDataQuery, BaseFormCreate, BaseFormDelete, diff --git a/shortcuts/base/workflow_button_fields.go b/shortcuts/base/workflow_button_fields.go new file mode 100644 index 0000000000..ae554fef13 --- /dev/null +++ b/shortcuts/base/workflow_button_fields.go @@ -0,0 +1,35 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "context" + + "github.com/larksuite/cli/shortcuts/common" +) + +var BaseWorkflowButtonFields = common.Shortcut{ + Service: "base", + Command: "+workflow-button-fields", + Description: "List button fields bound to a workflow", + Risk: "read", + Scopes: []string{"base:field:read", "base:workflow:read"}, + AuthTypes: authTypes(), + Flags: []common.Flag{ + {Name: "base-token", Desc: "base token", Required: true}, + {Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true}, + }, + Tips: []string{ + `Example: lark-cli base +workflow-button-fields --base-token --workflow-id `, + "workflow-id must be a wkf... OpenAPI ID; internal numeric workflow IDs are not accepted or displayed.", + "Returns table_id and field_id pairs bound through the button workflow relation.", + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + return validateWorkflowIDFlag(runtime) + }, + DryRun: dryRunWorkflowButtonFields, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + return executeWorkflowButtonFields(runtime) + }, +} diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index e040bab83b..1691cf7c60 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -66,12 +66,14 @@ metadata: | 一次性聚合统计 | `+data-query` | 必读 [lark-base-data-analysis-sop.md](references/lark-base-data-analysis-sop.md) 和入口 [lark-base-data-query-guide.md](references/lark-base-data-query-guide.md);完整 DSL 再读 [lark-base-data-query.md](references/lark-base-data-query.md) | | 公式字段 | `+field-create/update --json '{"type":"formula",...}'` | 必读 [formula-field-guide.md](references/formula-field-guide.md),读后再加隐藏确认 flag `--i-have-read-guide` | | Lookup 字段 | `+field-create/update --json '{"type":"lookup",...}'` | 必读 [lookup-field-guide.md](references/lookup-field-guide.md),读后再加隐藏确认 flag `--i-have-read-guide` | +| 按钮字段 | `+workflow-create` → `+field-create --json '{"type":"button",...}'` → `+field-button-bind` | 先创建 disabled 的 ButtonTrigger Workflow,再创建不含 Workflow ID 的按钮字段,然后用 `+field-button-bind` 绑定;用 `+field-button-binding-get` / `+workflow-button-fields` 查询确认,最后按需 `+workflow-enable` | | 表单提交 | `+form-submit` | 先读 [lark-base-form-detail.md](references/lark-base-form-detail.md) 获取题目、filter 和附件所需 `base_token`;提交 JSON 读 [lark-base-form-submit.md](references/lark-base-form-submit.md) | | 表单题目创建/更新 | `+form-questions-create` / `+form-questions-update` | Base 内表单按 table 管理;先确定并复用真实 `table_id`。读 [lark-base-form-questions-create.md](references/lark-base-form-questions-create.md) / [lark-base-form-questions-update.md](references/lark-base-form-questions-update.md);题目显隐条件 `visible_rule` 结构见公共协议 [lark-base-filter-condition.md](references/lark-base-filter-condition.md) | | Base 内表单管理 | `+form-list/get/create/update/delete` / `+form-questions-list/delete` | 缺少或不确定归属时,先用 `+table-list` 或 `+base-block-list` 取得真实 `table_id`;这些命令使用 `--base-token + --table-id` 并在整个工作流中复用同一 `table_id`,删除前确认目标表单 | | 分享表单详情 | `+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 与启停状态 | +| 按钮字段绑定 | `+field-button-bind` / `+field-button-binding-get` / `+field-button-unbind` / `+workflow-button-fields` | 只接受和展示 `wkf...` workflow ID;绑定事实来自独立按钮绑定接口,不来自字段 `property.trigger` | | 高级权限与角色 | `+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-create.md b/skills/lark-base/references/lark-base-field-create.md index e7c7bd03f3..ef8af4158c 100644 --- a/skills/lark-base/references/lark-base-field-create.md +++ b/skills/lark-base/references/lark-base-field-create.md @@ -35,6 +35,12 @@ lark-cli base +field-create \ --base-token \ --table-id \ --json '[{"name":"备注","type":"text"},{"name":"优先级","type":"select","multiple":false,"options":[{"name":"高"},{"name":"低"}]}]' + +# 按钮字段只创建静态字段配置,Workflow 绑定走 +field-button-bind +lark-cli base +field-create \ + --base-token \ + --table-id \ + --json '{"name":"提交审批","type":"button","button_text":"提交"}' ``` ## 参数 @@ -65,6 +71,7 @@ POST /open-apis/base/v3/bases/:base_token/tables/:table_id/fields - `link`:必须有 `link_table`,可选 `bidirectional`、`bidirectional_link_field_name`。 - `formula`:必须有 `expression`;先读 formula guide,再创建。 - `lookup`:必须有 `from`、`select`、`where`;先读 lookup guide,再创建。 + - `button`:只写按钮展示配置,例如 `button_text`;不要写 `workflow_id` 或 `property.trigger`,绑定必须在字段创建后调用 `+field-button-bind`。 **正确(base +field-create)** @@ -106,11 +113,13 @@ POST /open-apis/base/v3/bases/:base_token/tables/:table_id/fields 1. formula / lookup 字段必须先阅读对应指南;没读之前不要直接创建。 2. 创建简单字段时,优先相信命令返回;只有用户要求精确核对额外属性,或返回建议读回时,才继续执行 `+field-get`。 +3. 按钮字段的完整流程是:`+workflow-create` 创建 disabled 的 ButtonTrigger Workflow,`+field-create` 创建不含 Workflow ID 的按钮字段,`+field-button-bind` 建立绑定,`+field-button-binding-get` 查询确认,再 `+workflow-enable` 启用 Workflow。 ## 坑点 - ⚠️ 这是写入操作,执行前必须确认。 - ⚠️ 当 `type` 是 `formula` 或 `lookup` 时,先读对应 guide,再创建。 +- ⚠️ 当 `type` 是 `button` 时,字段 JSON 不建立 Workflow 绑定;不要通过 `property.trigger.config.id` 换绑。 - ⚠️ 不要把“每次创建后都 `+field-get`”当作固定流程;按返回里的 `field_get_recommended` 和 `next_step` 决定是否读回。 ## 参考 diff --git a/skills/lark-base/references/lark-base-field-json.md b/skills/lark-base/references/lark-base-field-json.md index 5667ca0bf4..25c9460b01 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_text` `button_color` | 所有类型都可额外传 `description`;上表的“常见补充字段”只列类型特有配置。 @@ -510,6 +511,34 @@ { "type": "checkbox", "name": "完成" } ``` +### 3.13 button + +按钮字段用于在记录上触发 Workflow。字段 JSON 只描述按钮字段的静态展示配置;不要在字段 JSON 中写 `workflow_id`、`trigger.config.id` 或其他绑定信息。 + +最小写法: + +```json +{ + "type": "button", + "name": "提交审批", + "button_text": "提交" +} +``` + +完整推荐流程: + +1. 用 `+workflow-create` 创建包含 `ButtonTrigger` 的 disabled Workflow,记录返回的 `wkf...` workflow ID。 +2. 用 `+field-create` 创建按钮字段,JSON 中不携带 Workflow ID。 +3. 用 `+field-button-bind --field-id --workflow-id ` 建立绑定。 +4. 用 `+field-button-binding-get` 或 `+workflow-button-fields` 查询确认绑定。 +5. 确认成功后再用 `+workflow-enable` 启用 Workflow。 + +常用字段: +- `button_text`:按钮上显示的文案。 +- `button_color`:按钮颜色;按服务端当前支持值传入,平台拒绝时按错误提示修正。 + +绑定关系以按钮 Workflow 绑定接口为准,字段查询里兼容回显的 `property.trigger` 只读参考,不能用于创建或换绑。 + ## 4. 创建与更新 - `+field-create`:按目标字段配置直接构造 `--json`。 @@ -517,7 +546,7 @@ ## 5. 暂不支持字段 -Object(对象字段)、Button(按钮字段)、Stage(流程字段)暂时都没有被 CLI 支持。这些字段会展示为 `not_support` 字段并被保护:不允许修改,不允许读取内容。 +Object(对象字段)、Stage(流程字段)暂时没有被 CLI 支持。这些字段会展示为 `not_support` 字段并被保护:不允许修改,不允许读取内容。 遇到暂不支持的字段类型时,直接说明 Base CLI 当前不支持并停止;不要猜测未注册的字段 JSON、service 或 schema,也不要用其他字段类型冒充目标能力。 diff --git a/tests/cli_e2e/base/base_field_dryrun_test.go b/tests/cli_e2e/base/base_field_dryrun_test.go index a98f3fc09b..46e19f1ebc 100644 --- a/tests/cli_e2e/base/base_field_dryrun_test.go +++ b/tests/cli_e2e/base/base_field_dryrun_test.go @@ -42,3 +42,81 @@ func TestBaseFieldCreateDryRunArrayCompat(t *testing.T) { require.Equal(t, "B", clie2e.DryRunGet(out, "api.1.body.name").String(), out) require.Equal(t, "text", clie2e.DryRunGet(out, "api.1.body.type").String(), out) } + +func TestBaseFieldButtonWorkflowDryRun(t *testing.T) { + setBaseDryRunConfigEnv(t) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + t.Cleanup(cancel) + + cases := []struct { + name string + args []string + method string + url string + }{ + { + name: "bind", + args: []string{ + "base", "+field-button-bind", + "--base-token", "app_x", + "--table-id", "tbl_x", + "--field-id", "fld_x", + "--workflow-id", "wkf_x", + "--dry-run", + }, + method: "POST", + url: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_x/button-workflow:bind", + }, + { + name: "get field binding", + args: []string{ + "base", "+field-button-binding-get", + "--base-token", "app_x", + "--table-id", "tbl_x", + "--field-id", "fld_x", + "--dry-run", + }, + method: "GET", + url: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_x/button-workflow", + }, + { + name: "unbind", + args: []string{ + "base", "+field-button-unbind", + "--base-token", "app_x", + "--table-id", "tbl_x", + "--field-id", "fld_x", + "--dry-run", + }, + method: "POST", + url: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields/fld_x/button-workflow:unbind", + }, + { + name: "workflow button fields", + args: []string{ + "base", "+workflow-button-fields", + "--base-token", "app_x", + "--workflow-id", "wkf_x", + "--dry-run", + }, + method: "GET", + url: "/open-apis/base/v3/bases/app_x/workflows/wkf_x/button-fields", + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + result, err := clie2e.RunCmd(ctx, clie2e.Request{Args: tt.args, DefaultAs: "bot"}) + require.NoError(t, err) + result.AssertExitCode(t, 0) + + out := result.Stdout + require.Equal(t, tt.url, clie2e.DryRunGet(out, "api.0.url").String(), out) + require.Equal(t, tt.method, clie2e.DryRunGet(out, "api.0.method").String(), out) + if tt.name == "bind" { + require.Equal(t, "wkf_x", clie2e.DryRunGet(out, "api.0.body.workflow_id").String(), out) + } + }) + } +}