diff --git a/internal/suggest/suggest.go b/internal/suggest/suggest.go
index fe4471f6..8c7f5f94 100644
--- a/internal/suggest/suggest.go
+++ b/internal/suggest/suggest.go
@@ -7,7 +7,10 @@
// carrying their own copy.
package suggest
-import "sort"
+import (
+ "sort"
+ "strings"
+)
// Levenshtein computes the classic edit distance between two strings. It is
// rune-aware, so it is correct for multi-byte input.
@@ -51,22 +54,29 @@ func Levenshtein(a, b string) int {
// signal of intent that raw edit distance misses.
func Closest(typed string, candidates []string, maxN int) []string {
type scored struct {
- name string
- prefix int
- dist int
+ name string
+ contain bool
+ prefix int
+ dist int
}
limit := editLimit(typed)
ranked := make([]scored, 0, len(candidates))
for _, c := range candidates {
p := sharedPrefixLen(typed, c)
d := Levenshtein(typed, c)
- // Keep only plausible matches: a meaningful shared prefix, or an edit
- // distance within budget. Drop everything else so the hint stays short.
- if p >= 3 || d <= limit {
- ranked = append(ranked, scored{name: c, prefix: p, dist: d})
+ ct := containsSegment(typed, c)
+ // Keep only plausible matches: a meaningful shared prefix, an edit
+ // distance within budget, or one name containing the other (a missing
+ // namespace prefix like "+block-list" vs "+base-block-list"). Drop
+ // everything else so the hint stays short.
+ if p >= 3 || d <= limit || ct {
+ ranked = append(ranked, scored{name: c, contain: ct, prefix: p, dist: d})
}
}
sort.Slice(ranked, func(i, j int) bool {
+ if ranked[i].contain != ranked[j].contain {
+ return ranked[i].contain
+ }
if ranked[i].prefix != ranked[j].prefix {
return ranked[i].prefix > ranked[j].prefix
}
@@ -94,6 +104,21 @@ func editLimit(s string) int {
return 2
}
+// containsSegment reports whether one name contains the other as a substring
+// after stripping the "+"/"--" sigils. It catches hallucinated names that drop
+// a namespace prefix (e.g. "+block-list" for "+base-block-list"), which share
+// almost no prefix and sit far beyond the edit-distance budget. The shorter
+// side must be at least 5 runes so generic fragments like "list" do not match
+// half the catalog.
+func containsSegment(a, b string) bool {
+ a = strings.TrimLeft(a, "+-")
+ b = strings.TrimLeft(b, "+-")
+ if len([]rune(a)) > len([]rune(b)) {
+ a, b = b, a
+ }
+ return len([]rune(a)) >= 5 && strings.Contains(b, a)
+}
+
func sharedPrefixLen(a, b string) int {
ra, rb := []rune(a), []rune(b)
n := 0
diff --git a/shortcuts/base/base_data_query.go b/shortcuts/base/base_data_query.go
index 616680a7..51565d26 100644
--- a/shortcuts/base/base_data_query.go
+++ b/shortcuts/base/base_data_query.go
@@ -7,6 +7,7 @@ import (
"bytes"
"context"
"encoding/json"
+ "strings"
"github.com/larksuite/cli/shortcuts/common"
)
@@ -20,6 +21,7 @@ var BaseDataQuery = common.Shortcut{
AuthTypes: authTypes(),
Flags: []common.Flag{
baseTokenFlag(true),
+ {Name: "table-id", Hidden: true},
{Name: "dsl", Desc: "query JSON DSL; read lark-base-data-query-guide.md first, then lark-base-data-query.md for the full DSL SSOT", Required: true},
},
Tips: []string{
@@ -28,6 +30,9 @@ var BaseDataQuery = common.Shortcut{
"`dimensions` and `measures` cannot both be empty.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
+ if strings.TrimSpace(runtime.Str("table-id")) != "" {
+ return baseFlagErrorf("+data-query does not support --table-id; put table names/fields inside --dsl (read lark-base-data-query-guide.md)")
+ }
var dsl map[string]interface{}
dec := json.NewDecoder(bytes.NewReader([]byte(runtime.Str("dsl"))))
dec.UseNumber()
diff --git a/shortcuts/base/base_dryrun_ops_test.go b/shortcuts/base/base_dryrun_ops_test.go
index 8f34fb11..cae725d8 100644
--- a/shortcuts/base/base_dryrun_ops_test.go
+++ b/shortcuts/base/base_dryrun_ops_test.go
@@ -73,6 +73,14 @@ func TestDryRunFieldOps(t *testing.T) {
)
assertDryRunContains(t, dryRunFieldList(ctx, listRT), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/fields", "offset=0", "limit=200")
+ batchListRT := newBaseTestRuntimeWithArrays(
+ map[string]string{"base-token": "app_x"},
+ map[string][]string{"table-id": {"tbl_1", "tbl_2"}},
+ nil,
+ map[string]int{"offset": 0, "limit": 50},
+ )
+ assertDryRunContains(t, dryRunFieldListBatch(ctx, batchListRT), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/fields", "GET /open-apis/base/v3/bases/app_x/tables/tbl_2/fields", "limit=50")
+
rt := newBaseTestRuntime(
map[string]string{
"base-token": "app_x",
diff --git a/shortcuts/base/base_execute_test.go b/shortcuts/base/base_execute_test.go
index 79bd12ac..baf402c8 100644
--- a/shortcuts/base/base_execute_test.go
+++ b/shortcuts/base/base_execute_test.go
@@ -1066,6 +1066,129 @@ func TestBaseFieldExecuteCRUD(t *testing.T) {
}
})
+ t.Run("list resolves table name", func(t *testing.T) {
+ factory, stdout, reg := newExecuteFactory(t)
+ reg.Register(&httpmock.Stub{
+ Method: "GET",
+ URL: "/open-apis/base/v3/bases/app_x/tables",
+ Body: map[string]interface{}{
+ "code": 0,
+ "data": map[string]interface{}{"tables": []interface{}{
+ map[string]interface{}{"id": "tbl_orders", "name": "Orders"},
+ }, "total": 1},
+ },
+ })
+ reg.Register(&httpmock.Stub{
+ Method: "GET",
+ URL: "/open-apis/base/v3/bases/app_x/tables/tbl_orders/fields",
+ Body: map[string]interface{}{
+ "code": 0,
+ "data": map[string]interface{}{"fields": []interface{}{
+ map[string]interface{}{"id": "fld_name", "name": "Name", "type": "text"},
+ }, "total": 1},
+ },
+ })
+ if err := runShortcut(t, BaseFieldList, []string{"+field-list", "--base-token", "app_x", "--table-id", "Orders"}, factory, stdout); err != nil {
+ t.Fatalf("err=%v", err)
+ }
+ if got := stdout.String(); !strings.Contains(got, `"fields"`) || !strings.Contains(got, `"name": "Name"`) {
+ t.Fatalf("stdout=%s", got)
+ }
+ })
+
+ t.Run("list batch multiple tables", 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_a/fields",
+ Body: map[string]interface{}{
+ "code": 0,
+ "data": map[string]interface{}{"fields": []interface{}{
+ map[string]interface{}{"id": "fld_a", "name": "Name", "type": "text", "style": map[string]interface{}{"type": "plain"}},
+ }, "total": 1},
+ },
+ })
+ reg.Register(&httpmock.Stub{
+ Method: "GET",
+ URL: "/open-apis/base/v3/bases/app_x/tables/tbl_b/fields",
+ Body: map[string]interface{}{
+ "code": 0,
+ "data": map[string]interface{}{"fields": []interface{}{
+ map[string]interface{}{"id": "fld_b", "name": "Status", "type": "select", "options": []interface{}{map[string]interface{}{"name": "Todo", "color": "red"}}},
+ }, "total": 1},
+ },
+ })
+ if err := runShortcut(t, BaseFieldListBatch, []string{"+field-list-batch", "--base-token", "app_x", "--table-id", "tbl_a", "--table-id", "tbl_b", "--compact"}, factory, stdout); err != nil {
+ t.Fatalf("err=%v", err)
+ }
+ got := stdout.String()
+ if !strings.Contains(got, `"tables"`) || !strings.Contains(got, `"table_id": "tbl_a"`) || !strings.Contains(got, `"table_id": "tbl_b"`) || !strings.Contains(got, `"options": [`) || !strings.Contains(got, `"Todo"`) || !strings.Contains(got, `"style"`) || strings.Contains(got, `"color"`) {
+ t.Fatalf("stdout=%s", got)
+ }
+ })
+
+ t.Run("list batch resolves table names", func(t *testing.T) {
+ factory, stdout, reg := newExecuteFactory(t)
+ reg.Register(&httpmock.Stub{
+ Method: "GET",
+ URL: "/open-apis/base/v3/bases/app_x/tables",
+ Body: map[string]interface{}{
+ "code": 0,
+ "data": map[string]interface{}{"tables": []interface{}{
+ map[string]interface{}{"id": "tbl_orders", "name": "Orders"},
+ }, "total": 1},
+ },
+ })
+ reg.Register(&httpmock.Stub{
+ Method: "GET",
+ URL: "/open-apis/base/v3/bases/app_x/tables/tbl_a/fields",
+ Body: map[string]interface{}{
+ "code": 0,
+ "data": map[string]interface{}{"fields": []interface{}{
+ map[string]interface{}{"id": "fld_a", "name": "Name", "type": "text"},
+ }, "total": 1},
+ },
+ })
+ reg.Register(&httpmock.Stub{
+ Method: "GET",
+ URL: "/open-apis/base/v3/bases/app_x/tables/tbl_orders/fields",
+ Body: map[string]interface{}{
+ "code": 0,
+ "data": map[string]interface{}{"fields": []interface{}{
+ map[string]interface{}{"id": "fld_order", "name": "Status", "type": "select"},
+ }, "total": 1},
+ },
+ })
+ if err := runShortcut(t, BaseFieldListBatch, []string{"+field-list-batch", "--base-token", "app_x", "--table-id", "tbl_a", "--table-id", "Orders"}, factory, stdout); err != nil {
+ t.Fatalf("err=%v", err)
+ }
+ got := stdout.String()
+ if !strings.Contains(got, `"table_id": "tbl_a"`) || !strings.Contains(got, `"table_id": "tbl_orders"`) || !strings.Contains(got, `"table_ref": "Orders"`) || !strings.Contains(got, `"table_name": "Orders"`) {
+ t.Fatalf("stdout=%s", got)
+ }
+ })
+
+ t.Run("list batch default keeps full fields", 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_b/fields",
+ Body: map[string]interface{}{
+ "code": 0,
+ "data": map[string]interface{}{"fields": []interface{}{
+ map[string]interface{}{"id": "fld_b", "name": "Status", "type": "select", "options": []interface{}{map[string]interface{}{"name": "Todo", "color": "red"}}},
+ }, "total": 1},
+ },
+ })
+ if err := runShortcut(t, BaseFieldListBatch, []string{"+field-list-batch", "--base-token", "app_x", "--table-id", "tbl_b"}, factory, stdout); err != nil {
+ t.Fatalf("err=%v", err)
+ }
+ got := stdout.String()
+ if !strings.Contains(got, `"table_id": "tbl_b"`) || !strings.Contains(got, `"color": "red"`) {
+ t.Fatalf("stdout=%s", got)
+ }
+ })
+
t.Run("get", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
@@ -1434,6 +1557,48 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) {
}
})
+ t.Run("search accepts query alias", func(t *testing.T) {
+ factory, stdout, reg := newExecuteFactory(t)
+ searchStub := &httpmock.Stub{
+ Method: "POST",
+ URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/search",
+ Body: map[string]interface{}{
+ "code": 0,
+ "data": map[string]interface{}{
+ "fields": []interface{}{"Title"},
+ "field_id_list": []interface{}{"fld_title"},
+ "record_id_list": []interface{}{"rec_1"},
+ "data": []interface{}{[]interface{}{"Created by AI"}},
+ "has_more": false,
+ },
+ },
+ }
+ reg.Register(searchStub)
+ if err := runShortcut(
+ t,
+ BaseRecordSearch,
+ []string{
+ "+record-search",
+ "--base-token", "app_x",
+ "--table-id", "tbl_x",
+ "--query", "Created",
+ "--search-field", "Title",
+ "--format", "json",
+ },
+ factory,
+ stdout,
+ ); err != nil {
+ t.Fatalf("err=%v", err)
+ }
+ var body map[string]interface{}
+ if err := json.Unmarshal(searchStub.CapturedBody, &body); err != nil {
+ t.Fatalf("captured body json err=%v body=%s", err, string(searchStub.CapturedBody))
+ }
+ if body["keyword"] != "Created" {
+ t.Fatalf("captured body=%#v", body)
+ }
+ })
+
t.Run("search with filter json file", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
tmp := t.TempDir()
@@ -1525,20 +1690,29 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) {
}
})
- t.Run("list legacy fields flag rejected", func(t *testing.T) {
- factory, stdout, _ := newExecuteFactory(t)
- err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--fields", "Name"}, factory, stdout)
- if err == nil || !strings.Contains(err.Error(), "unknown flag: --fields") {
+ t.Run("list fields alias projects columns", 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/records?field_id=Name&limit=100&offset=0",
+ Body: map[string]interface{}{
+ "code": 0,
+ "data": map[string]interface{}{"items": []interface{}{}, "has_more": false},
+ },
+ })
+ if err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--fields", "Name"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
})
- t.Run("list legacy fields flag rejected in dry-run", func(t *testing.T) {
+ t.Run("list fields alias works in dry-run", func(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
- err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--fields", "Name", "--dry-run"}, factory, stdout)
- if err == nil || !strings.Contains(err.Error(), "unknown flag: --fields") {
+ if err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--fields", "Name", "--dry-run"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
+ if got := stdout.String(); !strings.Contains(got, "field_id=Name") {
+ t.Fatalf("stdout=%s", got)
+ }
})
t.Run("get", func(t *testing.T) {
diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go
index 05f51da3..0bdcecfd 100644
--- a/shortcuts/base/base_shortcuts_test.go
+++ b/shortcuts/base/base_shortcuts_test.go
@@ -59,6 +59,23 @@ func newBaseTestRuntimeWithArrays(stringFlags map[string]string, stringArrayFlag
return &common.RuntimeContext{Cmd: cmd, Config: &core.CliConfig{UserOpenId: "ou_test"}}
}
+func TestFieldSearchOptionsAlias(t *testing.T) {
+ runtime := newBaseTestRuntime(map[string]string{"field-name": "Status"}, nil, nil)
+ if got := fieldSearchOptionsRef(runtime); got != "Status" {
+ t.Fatalf("field ref=%q", got)
+ }
+ if err := BaseFieldSearchOptions.Validate(context.Background(), runtime); err != nil {
+ t.Fatalf("err=%v", err)
+ }
+}
+
+func TestFieldSearchOptionsRequiresFieldRef(t *testing.T) {
+ err := BaseFieldSearchOptions.Validate(context.Background(), newBaseTestRuntime(map[string]string{}, nil, nil))
+ if err == nil || !strings.Contains(err.Error(), "--field-id is required") {
+ t.Fatalf("err=%v", err)
+ }
+}
+
func TestBaseAction(t *testing.T) {
t.Run("missing action", func(t *testing.T) {
runtime := newBaseTestRuntime(map[string]string{"get": ""}, map[string]bool{"list": false}, nil)
@@ -135,7 +152,7 @@ func TestShortcutsCatalog(t *testing.T) {
want := []string{
"+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",
- "+field-list", "+field-get", "+field-create", "+field-update", "+field-delete", "+field-search-options",
+ "+field-list", "+field-list-batch", "+field-get", "+field-create", "+field-update", "+field-delete", "+field-search-options",
"+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",
@@ -1088,6 +1105,22 @@ func TestBaseRecordValidate(t *testing.T) {
)); err != nil {
t.Fatalf("record search flag validate err=%v", err)
}
+ if err := BaseRecordSearch.Validate(ctx, newBaseTestRuntimeWithArrays(
+ map[string]string{"base-token": "b", "table-id": "tbl_1", "query": "Alice"},
+ map[string][]string{"search-field": {"Name"}},
+ nil,
+ nil,
+ )); err != nil {
+ t.Fatalf("record search query alias validate err=%v", err)
+ }
+ if err := BaseRecordSearch.Validate(ctx, newBaseTestRuntimeWithArrays(
+ map[string]string{"base-token": "b", "table-id": "tbl_1", "keyword": "Alice", "query": "Bob"},
+ map[string][]string{"search-field": {"Name"}},
+ nil,
+ nil,
+ )); err == nil || !strings.Contains(err.Error(), "use only one") {
+ t.Fatalf("err=%v", err)
+ }
if err := BaseRecordSearch.Validate(ctx, newBaseTestRuntime(
map[string]string{
"base-token": "b",
diff --git a/shortcuts/base/dashboard_block_get_data.go b/shortcuts/base/dashboard_block_get_data.go
index 3ee3b5a6..0074860f 100644
--- a/shortcuts/base/dashboard_block_get_data.go
+++ b/shortcuts/base/dashboard_block_get_data.go
@@ -19,6 +19,7 @@ var BaseDashboardBlockGetData = common.Shortcut{
HasFormat: true,
Flags: []common.Flag{
baseTokenFlag(true),
+ {Name: "dashboard-id", Hidden: true},
blockIDFlag(true),
},
Tips: []string{
diff --git a/shortcuts/base/field_list.go b/shortcuts/base/field_list.go
index 7851a6f9..5b135d4f 100644
--- a/shortcuts/base/field_list.go
+++ b/shortcuts/base/field_list.go
@@ -21,6 +21,7 @@ var BaseFieldList = common.Shortcut{
tableRefFlag(true),
{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
{Name: "limit", Type: "int", Default: "100", Desc: "pagination size, range 1-200"},
+ {Name: "compact", Type: "bool", Desc: "return compact field objects (id/name/type/style/options) for lower context cost; default returns full field objects"},
},
DryRun: dryRunFieldList,
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
diff --git a/shortcuts/base/field_list_batch.go b/shortcuts/base/field_list_batch.go
new file mode 100644
index 00000000..c45ed3f1
--- /dev/null
+++ b/shortcuts/base/field_list_batch.go
@@ -0,0 +1,30 @@
+// Copyright (c) 2026 Lark Technologies Pte. Ltd.
+// SPDX-License-Identifier: MIT
+
+package base
+
+import (
+ "context"
+
+ "github.com/larksuite/cli/shortcuts/common"
+)
+
+var BaseFieldListBatch = common.Shortcut{
+ Service: "base",
+ Command: "+field-list-batch",
+ Description: "List fields for multiple tables in one call",
+ Risk: "read",
+ Scopes: []string{"base:field:read"},
+ AuthTypes: authTypes(),
+ Flags: []common.Flag{
+ baseTokenFlag(true),
+ {Name: "table-id", Type: "string_array", Desc: tableRefFlag(true).Desc + "; repeat to list fields for multiple tables", Required: true},
+ {Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
+ {Name: "limit", Type: "int", Default: "100", Desc: "pagination size, range 1-200"},
+ {Name: "compact", Type: "bool", Desc: "return compact field objects (id/name/type/style/options) for lower context cost; default returns full field objects"},
+ },
+ DryRun: dryRunFieldListBatch,
+ Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
+ return executeFieldListBatch(runtime)
+ },
+}
diff --git a/shortcuts/base/field_ops.go b/shortcuts/base/field_ops.go
index 4774c95a..84815da6 100644
--- a/shortcuts/base/field_ops.go
+++ b/shortcuts/base/field_ops.go
@@ -10,6 +10,12 @@ import (
"github.com/larksuite/cli/shortcuts/common"
)
+type fieldListTableRef struct {
+ input string
+ id string
+ name string
+}
+
func dryRunFieldList(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
offset := runtime.Int("offset")
if offset < 0 {
@@ -23,6 +29,22 @@ func dryRunFieldList(_ context.Context, runtime *common.RuntimeContext) *common.
Set("table_id", baseTableID(runtime))
}
+func dryRunFieldListBatch(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
+ offset := runtime.Int("offset")
+ if offset < 0 {
+ offset = 0
+ }
+ limit := common.ParseIntBounded(runtime, "limit", 1, 200)
+ dry := common.NewDryRunAPI()
+ for _, tableIDValue := range runtime.StrArray("table-id") {
+ dry.GET(baseV3Path("bases", runtime.Str("base-token"), "tables", tableIDValue, "fields")).
+ Params(map[string]interface{}{"offset": offset, "limit": limit}).
+ Set("base_token", runtime.Str("base-token")).
+ Set("table_id", tableIDValue)
+ }
+ return dry
+}
+
func dryRunFieldGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
return common.NewDryRunAPI().
GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id").
@@ -61,6 +83,7 @@ func dryRunFieldDelete(_ context.Context, runtime *common.RuntimeContext) *commo
}
func dryRunFieldSearchOptions(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
+ fieldRef := fieldSearchOptionsRef(runtime)
params := map[string]interface{}{
"offset": runtime.Int("offset"),
"limit": runtime.Int("limit"),
@@ -68,15 +91,15 @@ func dryRunFieldSearchOptions(_ context.Context, runtime *common.RuntimeContext)
if params["limit"].(int) <= 0 {
params["limit"] = 30
}
- if keyword := strings.TrimSpace(runtime.Str("keyword")); keyword != "" {
+ if keyword := strings.TrimSpace(fieldSearchOptionsKeyword(runtime)); keyword != "" {
params["query"] = keyword
}
return common.NewDryRunAPI().
- GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/fields/:field_id/options").
+ GET(baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "fields", fieldRef, "options")).
Params(params).
Set("base_token", runtime.Str("base-token")).
Set("table_id", baseTableID(runtime)).
- Set("field_id", runtime.Str("field-id"))
+ Set("field_id", fieldRef)
}
func validateFieldJSON(runtime *common.RuntimeContext) (map[string]interface{}, error) {
@@ -118,17 +141,142 @@ func executeFieldList(runtime *common.RuntimeContext) error {
offset = 0
}
limit := common.ParseIntBounded(runtime, "limit", 1, 200)
- fields, total, err := listAllFields(runtime, runtime.Str("base-token"), baseTableID(runtime), offset, limit)
+ baseToken := runtime.Str("base-token")
+ tableRef, err := resolveFieldListTableRefs(runtime, baseToken, []string{baseTableID(runtime)})
+ if err != nil {
+ return err
+ }
+ fields, total, err := listAllFields(runtime, baseToken, tableRef[0].id, offset, limit)
if err != nil {
return err
}
if total == 0 {
total = len(fields)
}
+ if runtime.Bool("compact") {
+ fields = compactFields(fields)
+ }
runtime.Out(map[string]interface{}{"fields": fields, "total": total}, nil)
return nil
}
+func executeFieldListBatch(runtime *common.RuntimeContext) error {
+ offset := runtime.Int("offset")
+ if offset < 0 {
+ offset = 0
+ }
+ limit := common.ParseIntBounded(runtime, "limit", 1, 200)
+ baseToken := runtime.Str("base-token")
+ tableRefs, err := resolveFieldListTableRefs(runtime, baseToken, runtime.StrArray("table-id"))
+ if err != nil {
+ return err
+ }
+ results := make([]map[string]interface{}, 0, len(tableRefs))
+ for _, tableRef := range tableRefs {
+ fields, total, err := listAllFields(runtime, baseToken, tableRef.id, offset, limit)
+ if err != nil {
+ return err
+ }
+ if total == 0 {
+ total = len(fields)
+ }
+ if runtime.Bool("compact") {
+ fields = compactFields(fields)
+ }
+ result := map[string]interface{}{
+ "table_id": tableRef.id,
+ "fields": fields,
+ "total": total,
+ }
+ if tableRef.input != tableRef.id {
+ result["table_ref"] = tableRef.input
+ }
+ if tableRef.name != "" {
+ result["table_name"] = tableRef.name
+ }
+ results = append(results, result)
+ }
+ runtime.Out(map[string]interface{}{"tables": results, "total": len(results)}, nil)
+ return nil
+}
+
+func resolveFieldListTableRefs(runtime *common.RuntimeContext, baseToken string, refs []string) ([]fieldListTableRef, error) {
+ if len(refs) == 0 {
+ return nil, baseValidationErrorf("--table-id is required")
+ }
+ resolved := make([]fieldListTableRef, 0, len(refs))
+ needsTableList := false
+ for _, raw := range refs {
+ ref := strings.TrimSpace(raw)
+ if ref == "" {
+ return nil, baseValidationErrorf("--table-id must not be empty")
+ }
+ if !isBaseTableID(ref) {
+ needsTableList = true
+ }
+ resolved = append(resolved, fieldListTableRef{input: ref, id: ref})
+ }
+ if !needsTableList {
+ return resolved, nil
+ }
+ tables, err := listEveryTable(runtime, baseToken)
+ if err != nil {
+ return nil, err
+ }
+ for i, tableRef := range resolved {
+ if isBaseTableID(tableRef.input) {
+ continue
+ }
+ table, err := resolveTableRef(tables, tableRef.input)
+ if err != nil {
+ return nil, baseValidationErrorf("table %q not found; run +table-list to verify the table name or pass the tbl... ID", tableRef.input)
+ }
+ tableIDValue := tableID(table)
+ if tableIDValue == "" {
+ return nil, baseValidationErrorf("table %q resolved without a table ID; run +table-list and pass the tbl... ID", tableRef.input)
+ }
+ resolved[i].id = tableIDValue
+ resolved[i].name = tableNameFromMap(table)
+ }
+ return resolved, nil
+}
+
+func isBaseTableID(ref string) bool {
+ return strings.HasPrefix(strings.TrimSpace(ref), "tbl")
+}
+
+// compactFields projects each field to the keys an agent needs for selection
+// (id / name / type / style, plus select option names), dropping formula
+// expressions and lookup internals that bloat agent context. Opt-in via
+// `--compact`; the default output keeps full field objects.
+func compactFields(fields []map[string]interface{}) []map[string]interface{} {
+ keep := []string{"id", "name", "type", "is_primary", "ui_type", "description", "style"}
+ out := make([]map[string]interface{}, 0, len(fields))
+ for _, f := range fields {
+ c := map[string]interface{}{}
+ for _, k := range keep {
+ if v, ok := f[k]; ok {
+ c[k] = v
+ }
+ }
+ if opts, ok := f["options"].([]interface{}); ok && len(opts) > 0 {
+ names := make([]interface{}, 0, len(opts))
+ for _, o := range opts {
+ if om, ok := o.(map[string]interface{}); ok {
+ if name, ok := om["name"]; ok {
+ names = append(names, name)
+ continue
+ }
+ }
+ names = append(names, o)
+ }
+ c["options"] = names
+ }
+ out = append(out, c)
+ }
+ return out
+}
+
func executeFieldGet(runtime *common.RuntimeContext) error {
baseToken := runtime.Str("base-token")
tableIDValue := baseTableID(runtime)
@@ -184,10 +332,25 @@ func executeFieldDelete(runtime *common.RuntimeContext) error {
return nil
}
+func fieldSearchOptionsRef(runtime *common.RuntimeContext) string {
+ fieldRef := runtime.Str("field-id")
+ if strings.TrimSpace(fieldRef) == "" {
+ fieldRef = runtime.Str("field-name")
+ }
+ return fieldRef
+}
+
+func fieldSearchOptionsKeyword(runtime *common.RuntimeContext) string {
+ if keyword := strings.TrimSpace(runtime.Str("keyword")); keyword != "" {
+ return keyword
+ }
+ return strings.TrimSpace(runtime.Str("query"))
+}
+
func executeFieldSearchOptions(runtime *common.RuntimeContext) error {
baseToken := runtime.Str("base-token")
tableIDValue := baseTableID(runtime)
- fieldRef := runtime.Str("field-id")
+ fieldRef := fieldSearchOptionsRef(runtime)
params := map[string]interface{}{
"offset": runtime.Int("offset"),
"limit": runtime.Int("limit"),
@@ -195,7 +358,7 @@ func executeFieldSearchOptions(runtime *common.RuntimeContext) error {
if params["limit"].(int) <= 0 {
params["limit"] = 30
}
- if keyword := strings.TrimSpace(runtime.Str("keyword")); keyword != "" {
+ if keyword := strings.TrimSpace(fieldSearchOptionsKeyword(runtime)); keyword != "" {
params["query"] = keyword
}
data, err := baseV3Call(runtime, "GET", baseV3Path("bases", baseToken, "tables", tableIDValue, "fields", fieldRef, "options"), params, nil)
@@ -210,7 +373,7 @@ func executeFieldSearchOptions(runtime *common.RuntimeContext) error {
runtime.Out(map[string]interface{}{
"field_id": fieldRef,
"field_name": fieldRef,
- "keyword": strings.TrimSpace(runtime.Str("keyword")),
+ "keyword": fieldSearchOptionsKeyword(runtime),
"options": options,
"total": total,
}, nil)
diff --git a/shortcuts/base/field_search_options.go b/shortcuts/base/field_search_options.go
index 1783d368..6d2775c4 100644
--- a/shortcuts/base/field_search_options.go
+++ b/shortcuts/base/field_search_options.go
@@ -5,6 +5,7 @@ package base
import (
"context"
+ "strings"
"github.com/larksuite/cli/shortcuts/common"
)
@@ -19,8 +20,10 @@ var BaseFieldSearchOptions = common.Shortcut{
Flags: []common.Flag{
baseTokenFlag(true),
tableRefFlag(true),
- fieldRefFlag(true),
+ fieldRefFlag(false),
+ {Name: "field-name", Hidden: true},
{Name: "keyword", Desc: "keyword for option query"},
+ {Name: "query", Hidden: true},
{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
{Name: "limit", Type: "int", Default: "30", Desc: "pagination size, default 30"},
},
@@ -29,6 +32,15 @@ var BaseFieldSearchOptions = common.Shortcut{
"Use only for fields with options, such as select or multi-select fields.",
},
DryRun: dryRunFieldSearchOptions,
+ Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
+ if strings.TrimSpace(fieldSearchOptionsRef(runtime)) == "" {
+ return baseFlagErrorf("--field-id is required")
+ }
+ if strings.TrimSpace(runtime.Str("keyword")) != "" && strings.TrimSpace(runtime.Str("query")) != "" {
+ return baseFlagErrorf("--query is a deprecated alias for --keyword; use only one")
+ }
+ return nil
+ },
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeFieldSearchOptions(runtime)
},
diff --git a/shortcuts/base/helpers_test.go b/shortcuts/base/helpers_test.go
index a49f0efc..6a7b4152 100644
--- a/shortcuts/base/helpers_test.go
+++ b/shortcuts/base/helpers_test.go
@@ -4,6 +4,7 @@
package base
import (
+ "context"
"encoding/json"
"os"
"reflect"
@@ -496,3 +497,61 @@ func TestCanonicalSelectAndCompareHelpers(t *testing.T) {
t.Fatalf("err=%v", err)
}
}
+
+func TestNormalizePluralReferenceValues(t *testing.T) {
+ cases := []struct {
+ name string
+ in []string
+ want []string
+ }{
+ {"repeated single values", []string{"fldA", "fldB"}, []string{"fldA", "fldB"}},
+ {"json array", []string{`["fldA","fldB"]`}, []string{"fldA", "fldB"}},
+ {"comma separated ids", []string{"fldA, fldB"}, []string{"fldA", "fldB"}},
+ {"comma separated names", []string{"商品名称,SKU,单价"}, []string{"商品名称", "SKU", "单价"}},
+ {"trailing comma ignored", []string{"recA,recB,"}, []string{"recA", "recB"}},
+ {"fullwidth comma kept whole", []string{"销售额,单价"}, []string{"销售额,单价"}},
+ {"mixed forms", []string{`["fldA"]`, "fldB,fldC", "Name"}, []string{"fldA", "fldB", "fldC", "Name"}},
+ {"invalid json kept literal", []string{`[fldA`}, []string{`[fldA`}},
+ {"blank dropped", []string{" ", "fldA"}, []string{"fldA"}},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ if got := normalizePluralReferenceValues(tc.in); !reflect.DeepEqual(got, tc.want) {
+ t.Fatalf("got=%v want=%v", got, tc.want)
+ }
+ })
+ }
+}
+
+func TestRecordFlagAliasMergeAndDedupe(t *testing.T) {
+ fieldRT := newBaseTestRuntimeWithArrays(nil, map[string][]string{
+ "field-id": {"fldA"},
+ "fields": {"fldA,fldB"},
+ }, nil, nil)
+ if got := recordFieldFlags(fieldRT); !reflect.DeepEqual(got, []string{"fldA", "fldB"}) {
+ t.Fatalf("field flags=%v", got)
+ }
+ recordRT := newBaseTestRuntimeWithArrays(nil, map[string][]string{
+ "record-id": {"recA"},
+ "record-ids": {`["recA","recB"]`},
+ }, nil, nil)
+ if got := recordIDFlags(recordRT); !reflect.DeepEqual(got, []string{"recA", "recB"}) {
+ t.Fatalf("record flags=%v", got)
+ }
+}
+
+func TestFieldSearchOptionsKeywordQueryAlias(t *testing.T) {
+ ctx := context.Background()
+ if err := BaseFieldSearchOptions.Validate(ctx, newBaseTestRuntime(
+ map[string]string{"field-id": "Status", "keyword": "A", "query": "B"}, nil, nil,
+ )); err == nil || !strings.Contains(err.Error(), "use only one") {
+ t.Fatalf("err=%v", err)
+ }
+ queryOnly := newBaseTestRuntime(map[string]string{"field-id": "Status", "query": "Do"}, nil, nil)
+ if err := BaseFieldSearchOptions.Validate(ctx, queryOnly); err != nil {
+ t.Fatalf("err=%v", err)
+ }
+ if got := fieldSearchOptionsKeyword(queryOnly); got != "Do" {
+ t.Fatalf("keyword=%q", got)
+ }
+}
diff --git a/shortcuts/base/record_get.go b/shortcuts/base/record_get.go
index f8d0b720..9b0e3e55 100644
--- a/shortcuts/base/record_get.go
+++ b/shortcuts/base/record_get.go
@@ -21,7 +21,10 @@ var BaseRecordGet = common.Shortcut{
baseTokenFlag(true),
tableRefFlag(true),
{Name: "record-id", Type: "string_array", Desc: "record ID (repeatable)"},
+ {Name: "record-ids", Type: "string_array", Hidden: true},
{Name: "field-id", Type: "string_array", Desc: "field ID or name to project; repeat to keep only needed columns"},
+ {Name: "field-names", Type: "string_array", Hidden: true},
+ {Name: "fields", Type: "string_array", Hidden: true},
{Name: "json", Desc: `JSON object with record_id_list, e.g. {"record_id_list":["rec_xxx"]}`},
recordReadFormatFlag(),
},
diff --git a/shortcuts/base/record_list.go b/shortcuts/base/record_list.go
index b6489a5c..678851d9 100644
--- a/shortcuts/base/record_list.go
+++ b/shortcuts/base/record_list.go
@@ -5,6 +5,7 @@ package base
import (
"context"
+ "strings"
"github.com/larksuite/cli/shortcuts/common"
"github.com/spf13/cobra"
@@ -21,9 +22,14 @@ var BaseRecordList = common.Shortcut{
baseTokenFlag(true),
tableRefFlag(true),
recordListFieldRefFlag(),
+ {Name: "field-names", Type: "string_array", Hidden: true},
+ {Name: "fields", Type: "string_array", Hidden: true},
recordListViewRefFlag(),
recordFilterFlag(),
+ recordFilterAliasFlag(),
recordSortFlag(),
+ recordSortAliasFlag(),
+ {Name: "json", Hidden: true},
{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
{Name: "limit", Type: "int", Default: "100", Desc: "pagination size, range 1-200"},
recordReadFormatFlag(),
@@ -45,6 +51,9 @@ var BaseRecordList = common.Shortcut{
if err := validateRecordReadFormat(runtime); err != nil {
return err
}
+ if strings.TrimSpace(runtime.Str("json")) != "" {
+ return baseFlagErrorf("+record-list does not support --json; use --filter-json for filters and --sort-json for sorting")
+ }
return validateRecordQueryOptions(runtime)
},
DryRun: dryRunRecordList,
diff --git a/shortcuts/base/record_ops.go b/shortcuts/base/record_ops.go
index 1f526409..ef49b3af 100644
--- a/shortcuts/base/record_ops.go
+++ b/shortcuts/base/record_ops.go
@@ -5,6 +5,7 @@ package base
import (
"context"
+ "encoding/json"
"net/url"
"strconv"
"strings"
@@ -45,11 +46,11 @@ func validateRecordSelection(runtime *common.RuntimeContext) error {
}
func resolveRecordSelection(runtime *common.RuntimeContext) (recordSelection, error) {
- recordIDs := runtime.StrArray("record-id")
- fieldIDs := runtime.StrArray("field-id")
+ recordIDs := recordIDFlags(runtime)
+ fieldIDs := recordFieldFlags(runtime)
jsonRaw := strings.TrimSpace(runtime.Str("json"))
if len(recordIDs) > 0 && jsonRaw != "" {
- return recordSelection{}, baseFlagErrorf("--record-id and --json are mutually exclusive")
+ return recordSelection{}, baseFlagErrorf("--record-id/--record-ids and --json are mutually exclusive")
}
if jsonRaw != "" {
pc := newParseCtx(runtime)
@@ -145,6 +146,73 @@ func normalizeRecordGetSelectFields(values interface{}) ([]string, error) {
})
}
+func recordIDFlags(runtime *common.RuntimeContext) []string {
+ return mergeReferenceSources(
+ runtime.StrArray("record-id"),
+ normalizePluralReferenceValues(runtime.StrArray("record-ids")),
+ )
+}
+
+func recordFieldFlags(runtime *common.RuntimeContext) []string {
+ return mergeReferenceSources(
+ runtime.StrArray("field-id"),
+ normalizePluralReferenceValues(runtime.StrArray("field-names")),
+ normalizePluralReferenceValues(runtime.StrArray("fields")),
+ )
+}
+
+// mergeReferenceSources concatenates flag sources, dropping values from later
+// sources that an earlier source already provided — so the same reference
+// passed through both a canonical flag and its plural alias is sent only once.
+// Duplicates inside a single source are kept on purpose: repeating a value on
+// one flag is a user mistake that downstream validation should keep rejecting.
+func mergeReferenceSources(sources ...[]string) []string {
+ var out []string
+ seenBefore := map[string]struct{}{}
+ for _, source := range sources {
+ for _, value := range source {
+ if _, ok := seenBefore[value]; ok {
+ continue
+ }
+ out = append(out, value)
+ }
+ for _, value := range source {
+ seenBefore[value] = struct{}{}
+ }
+ }
+ return out
+}
+
+// normalizePluralReferenceValues expands each raw value of a plural alias flag
+// (--field-names / --fields / --record-ids) into individual references. Plural
+// flags carry list semantics, so an ASCII comma is always a separator (eval
+// traces show comma-joined values are exclusively lists, mostly field names);
+// a JSON string array is also accepted. Names that contain a literal ASCII
+// comma must use the singular flag (--field-id), which never splits. Fullwidth
+// "," and "、" are untouched, so ordinary Chinese names are safe here too.
+func normalizePluralReferenceValues(values []string) []string {
+ var out []string
+ for _, value := range values {
+ value = strings.TrimSpace(value)
+ if value == "" {
+ continue
+ }
+ if strings.HasPrefix(value, "[") {
+ var parsed []string
+ if err := json.Unmarshal([]byte(value), &parsed); err == nil {
+ out = append(out, parsed...)
+ continue
+ }
+ }
+ for _, part := range strings.Split(value, ",") {
+ if part = strings.TrimSpace(part); part != "" {
+ out = append(out, part)
+ }
+ }
+ }
+ return out
+}
+
func normalizeStringList(values interface{}, opts stringListNormalizeOptions) ([]string, error) {
var rawItems []interface{}
switch typed := values.(type) {
@@ -375,7 +443,7 @@ func validateRecordJSON(runtime *common.RuntimeContext) error {
}
func recordListFields(runtime *common.RuntimeContext) []string {
- return runtime.StrArray("field-id")
+ return recordFieldFlags(runtime)
}
func executeRecordList(runtime *common.RuntimeContext) error {
diff --git a/shortcuts/base/record_query.go b/shortcuts/base/record_query.go
index 3341d66b..edde9af3 100644
--- a/shortcuts/base/record_query.go
+++ b/shortcuts/base/record_query.go
@@ -26,6 +26,10 @@ func recordFilterFlag() common.Flag {
}
}
+func recordFilterAliasFlag() common.Flag {
+ return common.Flag{Name: "filter", Hidden: true, Input: []string{common.File}}
+}
+
func recordSortFlag() common.Flag {
return common.Flag{
Name: recordSortJSONFlag,
@@ -34,6 +38,10 @@ func recordSortFlag() common.Flag {
}
}
+func recordSortAliasFlag() common.Flag {
+ return common.Flag{Name: "sort", Hidden: true, Input: []string{common.File}}
+}
+
func validateRecordQueryOptions(runtime *common.RuntimeContext) error {
if _, err := parseRecordFilterFlag(runtime); err != nil {
return err
@@ -43,7 +51,10 @@ func validateRecordQueryOptions(runtime *common.RuntimeContext) error {
}
func parseRecordFilterFlag(runtime *common.RuntimeContext) (interface{}, error) {
- filterRaw := strings.TrimSpace(runtime.Str(recordFilterJSONFlag))
+ filterRaw, err := recordQueryFlagValue(runtime, recordFilterJSONFlag, "filter")
+ if err != nil {
+ return nil, err
+ }
if filterRaw == "" {
return nil, nil
}
@@ -52,7 +63,10 @@ func parseRecordFilterFlag(runtime *common.RuntimeContext) (interface{}, error)
}
func parseRecordSortFlag(runtime *common.RuntimeContext) ([]interface{}, error) {
- sortRaw := strings.TrimSpace(runtime.Str(recordSortJSONFlag))
+ sortRaw, err := recordQueryFlagValue(runtime, recordSortJSONFlag, "sort")
+ if err != nil {
+ return nil, err
+ }
if sortRaw == "" {
return nil, nil
}
@@ -64,6 +78,18 @@ func parseRecordSortFlag(runtime *common.RuntimeContext) ([]interface{}, error)
return normalizeRecordSortValue(value, "--"+recordSortJSONFlag)
}
+func recordQueryFlagValue(runtime *common.RuntimeContext, canonical string, alias string) (string, error) {
+ canonicalRaw := strings.TrimSpace(runtime.Str(canonical))
+ aliasRaw := strings.TrimSpace(runtime.Str(alias))
+ if canonicalRaw != "" && aliasRaw != "" {
+ return "", baseFlagErrorf("--%s is a deprecated alias for --%s; use only one", alias, canonical)
+ }
+ if canonicalRaw != "" {
+ return canonicalRaw, nil
+ }
+ return aliasRaw, nil
+}
+
func normalizeRecordSortValue(value interface{}, label string) ([]interface{}, error) {
var sortConfig []interface{}
if parsed, ok := value.([]interface{}); ok {
@@ -167,7 +193,7 @@ func applyRecordQueryToBody(runtime *common.RuntimeContext, body map[string]inte
func recordSearchFlagBody(runtime *common.RuntimeContext) (map[string]interface{}, error) {
body := map[string]interface{}{}
- if keyword := strings.TrimSpace(runtime.Str("keyword")); keyword != "" {
+ if keyword := recordSearchKeyword(runtime); keyword != "" {
body["keyword"] = keyword
}
searchFields := runtime.StrArray("search-field")
@@ -217,6 +243,9 @@ func validateRecordSearchFlags(runtime *common.RuntimeContext) error {
if err := validateRecordReadFormat(runtime); err != nil {
return err
}
+ if strings.TrimSpace(runtime.Str("keyword")) != "" && strings.TrimSpace(runtime.Str("query")) != "" {
+ return baseFlagErrorf("--query is a deprecated alias for --keyword; use only one")
+ }
jsonRaw := strings.TrimSpace(runtime.Str("json"))
if jsonRaw != "" {
if recordSearchHasJSONExclusiveFlagInputs(runtime) {
@@ -225,7 +254,7 @@ func validateRecordSearchFlags(runtime *common.RuntimeContext) error {
_, err := recordSearchJSONBody(runtime)
return err
}
- if strings.TrimSpace(runtime.Str("keyword")) == "" {
+ if recordSearchKeyword(runtime) == "" {
return baseFlagErrorf("--keyword is required unless --json is used")
}
if len(runtime.StrArray("search-field")) == 0 {
@@ -235,7 +264,7 @@ func validateRecordSearchFlags(runtime *common.RuntimeContext) error {
}
func recordSearchHasJSONExclusiveFlagInputs(runtime *common.RuntimeContext) bool {
- return strings.TrimSpace(runtime.Str("keyword")) != "" ||
+ return recordSearchKeyword(runtime) != "" ||
len(runtime.StrArray("search-field")) > 0 ||
len(recordListFields(runtime)) > 0 ||
runtime.Str("view-id") != "" ||
@@ -243,6 +272,13 @@ func recordSearchHasJSONExclusiveFlagInputs(runtime *common.RuntimeContext) bool
runtime.Changed("limit")
}
+func recordSearchKeyword(runtime *common.RuntimeContext) string {
+ if keyword := strings.TrimSpace(runtime.Str("keyword")); keyword != "" {
+ return keyword
+ }
+ return strings.TrimSpace(runtime.Str("query"))
+}
+
func formatRecordQueryPriorityTip() string {
return fmt.Sprintf("Query priority: --%s overrides --view-id's view filter JSON; --%s overrides --view-id's view sort config.", recordFilterJSONFlag, recordSortJSONFlag)
}
diff --git a/shortcuts/base/record_search.go b/shortcuts/base/record_search.go
index 586e5071..ba2979e6 100644
--- a/shortcuts/base/record_search.go
+++ b/shortcuts/base/record_search.go
@@ -22,11 +22,16 @@ var BaseRecordSearch = common.Shortcut{
tableRefFlag(true),
{Name: "json", Desc: `record search JSON object for the full request body, e.g. {"keyword":"Alice","search_fields":["Name"],"select_fields":["Name","Status"],"filter":{"logic":"and","conditions":[]},"sort":[{"field":"Updated","desc":true}],"limit":50}; escape hatch for advanced cases`},
{Name: "keyword", Desc: "keyword for record search; required unless --json is used"},
+ {Name: "query", Desc: "deprecated alias for --keyword", Hidden: true},
{Name: "search-field", Type: "string_array", Desc: "field ID or name to search; repeat for multiple fields; required unless --json is used"},
recordListFieldRefFlag(),
+ {Name: "field-names", Type: "string_array", Hidden: true},
+ {Name: "fields", Type: "string_array", Hidden: true},
recordListViewRefFlag(),
recordFilterFlag(),
+ recordFilterAliasFlag(),
recordSortFlag(),
+ recordSortAliasFlag(),
{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
{Name: "limit", Type: "int", Default: "10", Desc: "pagination size, range 1-200"},
recordReadFormatFlag(),
diff --git a/shortcuts/base/shortcuts.go b/shortcuts/base/shortcuts.go
index 42fa883a..c1d135b6 100644
--- a/shortcuts/base/shortcuts.go
+++ b/shortcuts/base/shortcuts.go
@@ -19,6 +19,7 @@ func Shortcuts() []common.Shortcut {
BaseTableUpdate,
BaseTableDelete,
BaseFieldList,
+ BaseFieldListBatch,
BaseFieldGet,
BaseFieldCreate,
BaseFieldUpdate,
diff --git a/shortcuts/base/table_ops.go b/shortcuts/base/table_ops.go
index 5d986eab..f6a6c20d 100644
--- a/shortcuts/base/table_ops.go
+++ b/shortcuts/base/table_ops.go
@@ -63,7 +63,8 @@ func executeTableList(runtime *common.RuntimeContext) error {
offset = 0
}
limit := common.ParseIntBounded(runtime, "limit", 1, 100)
- tables, total, err := listAllTables(runtime, runtime.Str("base-token"), offset, limit)
+ baseToken := runtime.Str("base-token")
+ tables, total, err := listAllTables(runtime, baseToken, offset, limit)
if err != nil {
return err
}
@@ -186,6 +187,24 @@ func listEveryField(runtime *common.RuntimeContext, baseToken, tableID string) (
return items, nil
}
+func listEveryTable(runtime *common.RuntimeContext, baseToken string) ([]map[string]interface{}, error) {
+ const pageLimit = 100
+ offset := 0
+ items := []map[string]interface{}{}
+ for {
+ batch, total, err := listAllTables(runtime, baseToken, offset, pageLimit)
+ if err != nil {
+ return nil, err
+ }
+ items = append(items, batch...)
+ if len(batch) == 0 || len(batch) < pageLimit || (total > 0 && len(items) >= total) {
+ break
+ }
+ offset += len(batch)
+ }
+ return items, nil
+}
+
func listEveryView(runtime *common.RuntimeContext, baseToken, tableID string) ([]map[string]interface{}, error) {
const pageLimit = 100
offset := 0
diff --git a/shortcuts/drive/drive_import.go b/shortcuts/drive/drive_import.go
index 8c5c9a2c..17282beb 100644
--- a/shortcuts/drive/drive_import.go
+++ b/shortcuts/drive/drive_import.go
@@ -31,7 +31,10 @@ var DriveImport = common.Shortcut{
{Name: "type", Desc: "target document type (docx, sheet, bitable, slides)", Required: true},
{Name: "folder-token", Desc: "target folder token (omit for root folder; API accepts empty mount_key as root)"},
{Name: "name", Desc: "imported file name (default: local file name without extension)"},
- {Name: "target-token", Desc: "existing token to import data into (only for type=bitable); when set, data is mounted into this bitable instead of creating a new one"},
+ {Name: "target-token", Desc: "existing token to import data into (only for type=bitable); verify the returned verification_token, not the import task token"},
+ },
+ Tips: []string{
+ "When --target-token is set, data is mounted into that existing Base; verify output.verification_token with lark-cli base +base-get.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
return validateDriveImportSpec(driveImportSpec{
@@ -139,6 +142,14 @@ var DriveImport = common.Shortcut{
if status.Extra != nil {
out["extra"] = status.Extra
}
+ if spec.TargetToken != "" {
+ out["target_token"] = spec.TargetToken
+ out["verification_token"] = spec.TargetToken
+ if u := common.BuildResourceURL(runtime.Config.Brand, "bitable", spec.TargetToken); u != "" {
+ out["verification_url"] = u
+ }
+ out["verify_hint"] = fmt.Sprintf("because --target-token was used, verify the existing target Base with: lark-cli base +base-get --base-token %s", spec.TargetToken)
+ }
if !ready {
nextCommand := driveImportTaskResultCommand(ticket)
fmt.Fprintf(runtime.IO().ErrOut, "Import task is still in progress. Continue with: %s\n", nextCommand)
diff --git a/shortcuts/drive/drive_import_test.go b/shortcuts/drive/drive_import_test.go
index ba540753..f9b65da0 100644
--- a/shortcuts/drive/drive_import_test.go
+++ b/shortcuts/drive/drive_import_test.go
@@ -762,3 +762,43 @@ func TestDriveImportFallbackURLForSlides(t *testing.T) {
t.Fatalf("data.url = %#v, want %q (slides fallback)", got, want)
}
}
+
+func TestDriveImportTargetTokenOutputsVerificationToken(t *testing.T) {
+ f, stdout, _, reg := cmdutil.TestFactory(t, driveImportTestConfig("target-token"))
+ driveImportMockEnv(t, reg, "ticket_target", map[string]interface{}{
+ "token": "bascn_backend_result",
+ "type": "bitable",
+ "job_status": float64(0),
+ })
+
+ tmpDir := t.TempDir()
+ origDir, _ := os.Getwd()
+ if err := os.Chdir(tmpDir); err != nil {
+ t.Fatalf("Chdir: %v", err)
+ }
+ defer os.Chdir(origDir)
+ if err := os.WriteFile("snapshot.base", []byte("fake-base"), 0o644); err != nil {
+ t.Fatalf("WriteFile: %v", err)
+ }
+
+ if err := mountAndRunDrive(t, DriveImport, []string{
+ "+import", "--file", "snapshot.base", "--type", "bitable", "--target-token", "bascn_target", "--as", "user",
+ }, f, stdout); err != nil {
+ t.Fatalf("import should succeed, got: %v", err)
+ }
+
+ data := decodeDriveEnvelope(t, stdout)
+ if got, want := data["token"], "bascn_backend_result"; got != want {
+ t.Fatalf("data.token = %#v, want backend result token %q", got, want)
+ }
+ if got, want := data["verification_token"], "bascn_target"; got != want {
+ t.Fatalf("data.verification_token = %#v, want target token %q", got, want)
+ }
+ if got, want := data["target_token"], "bascn_target"; got != want {
+ t.Fatalf("data.target_token = %#v, want target token %q", got, want)
+ }
+ hint, _ := data["verify_hint"].(string)
+ if !strings.Contains(hint, "lark-cli base +base-get --base-token bascn_target") {
+ t.Fatalf("verify_hint = %q, want target-token verification command", hint)
+ }
+}
diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md
index f211dcb5..c1af3882 100644
--- a/skills/lark-base/SKILL.md
+++ b/skills/lark-base/SKILL.md
@@ -35,16 +35,44 @@ metadata:
- Base 命令必须先有 `base_token` 或可解析出的 Base URL。没有 token 时:用户要新建就用 `+base-create`;用户给标题/关键词就搜 `lark-cli drive +search --query "" --doc-types bitable --only-title --as user`;仍无法定位时,反问用户具体是哪一个 Base。
- 认证、初始化、scope、身份切换、权限不足恢复属于 `lark-shared`;Base 文档只保留会影响 Base 路径选择的权限规则。
+## 名词与概念
+
+| 名词 | 含义 |
+|---|---|
+| Base / 多维表格 / Bitable | 同一个东西:`/base/{token}` 链接对应的整个文档容器,token 即 `--base-token`;Bitable 是曾用名,只出现在历史 API 和返回字段里 |
+| Table(数据表) | Base 内的一张数据表,ID `tbl` 开头;列是 field,行是 record |
+| Field(字段)/ Record(记录) | 表的列与行;字段 ID `fld` 开头,记录 ID `rec` 开头 |
+| View(视图) | 同一张 table 的一种展示配置(筛选/排序/分组等),ID `viw` 开头 |
+| Form(表单) | 收集数据的入口,提交结果写入对应 table 的记录 |
+| Workflow(工作流) | Base 内的自动化流程,ID `wkf` 开头,由 steps(trigger + action)组成 |
+| Dashboard(仪表盘) | 数据可视化容器,ID `blk` 开头(因为它本身是 Base 资源目录里的一个 block,见下方歧义说明) |
+| Chart(图表/组件) | 又叫Dashboard block, 是 dashboard 内的单个可视化组件(柱状图/饼图/指标卡等), ID `cht` 开头 |
+| Base block (`+base-block-*`)| Base 资源目录里的节点,table/docx/dashboard/workflow/folder 在目录层面统称 block。 “这个 Base 里有哪些东西” → `+base-block-list`|
+
+**`block` 是易混淆词,同名不同义,按命令域区分:base-block 和 dashboard-block**
+
+### Base 心智模型
+
+- `base-block` 只负责资源目录管理,包括创建资源、移动到 folder、重命名和删除;具体资源内容仍走 table/dashboard/workflow 命令。
+- 新建 Base 时,强烈推荐一次性执行 `lark-cli base +base-create --name "" --table-name "
" --fields ''`,同时配置新 Base 里唯一一个初始数据表的 name 和 schema;使用 `--fields` 前先读 [lark-base-field-json.md](references/lark-base-field-json.md) 或复用 `+field-create` 的字段 JSON 形状,不要猜字段属性。
+- `+base-create` 不传 `--table-name` 和 `--fields` 时,会创建一个默认 schema 的初始数据表。
+- 表、字段、视图、workflow、dashboard block 的名称和 ID 必须来自真实返回,不要凭用户口述猜。
+- 存储字段可写;系统字段、`formula`、`lookup` 只读;附件字段走专用 attachment 命令。
+- 一次性原始记录查询优先用 `+record-list` / `+record-search` 的 filter/sort;聚合分析优先用 `+data-query`;需要长期显示在表中时,才新增 `formula` / `lookup` 字段。
+- `formula` 适合常规计算、条件判断、文本/日期处理和长期派生指标;`lookup` 适合明确的跨表查找、筛选后取值或聚合引用。
+- 写入、分析、公式、lookup、workflow、dashboard 前,先读取真实结构:表、字段、视图、关联表和 dashboard block 名称都以命令返回为准。
+- 跨表场景必须读取目标表结构;link 单元格中的关联 `record_id` 只是连接键,最终回答要回查并展示用户可读字段。
+
## 快速路由
| 用户目标 | 优先命令 | 何时读 reference |
|---|---|---|
| 查 Base 本体 | `+base-get` | 用返回确认 Base 名称、owner、权限和可继续操作的 token |
| 创建/复制 Base | `+base-create` / `+base-copy` | 新建时强烈推荐用 `--table-name` + `--fields` 同时配置新 Base 里唯一一个初始数据表的 name 和 schema;写入后报告新 Base 标识和 `permission_grant` |
-| 查看 Base 内资源目录 | `+base-block-list` | 想先了解一个 Base 里有哪些 table/docx/dashboard/workflow/folder 时优先用它;返回 ID 关系和 fewshot 看 `--help` |
+| 查看 Base 内资源目录 | `+base-block-list` | 先判断 Base 里有什么(table/docx/dashboard/workflow/folder),再决定走哪类命令 |
| 管理 Base 内资源目录 | `+base-block-create/move/rename/delete` | 创建或整理 Base 直接管理的 folder/table/docx/dashboard/workflow;资源内容继续用对应命令 |
| 管理数据表 | `+table-list/get/create/update/delete` | 处理 table 的列出、详情、创建、重命名和删除 |
-| 列/查/删字段 | `+field-list/get/delete/search-options` | 写入前用 list/get 确认字段类型、选项、ID;删除前确认目标字段 |
+| 列/查/删字段 | `+field-list/get/delete/search-options` | 字段发现默认用 `+field-list --compact`;需要 formula/lookup 细节或完整字段 JSON 再用 `+field-get` / 不带 compact 的 list;多表结构用 `+field-list-batch --compact --table-id <表1> --table-id <表2>` 一次取齐,不要逐表调用 |
| 创建/更新字段 | `+field-create` / `+field-update` | 必读 [lark-base-field-json.md](references/lark-base-field-json.md);公式读 [formula-field-guide.md](references/formula-field-guide.md);lookup 读 [lookup-field-guide.md](references/lookup-field-guide.md);命令细节读 [lark-base-field-create.md](references/lark-base-field-create.md) / [lark-base-field-update.md](references/lark-base-field-update.md) |
| 读记录明细 | `+record-get` / `+record-list` / `+record-search` | 涉及筛选、排序、Top/Bottom N、聚合、多表关联、全局结论时读 [lark-base-data-analysis-sop.md](references/lark-base-data-analysis-sop.md) |
| 写记录 | `+record-upsert` / `+record-batch-create` / `+record-batch-update` | 必读 [lark-base-record-upsert.md](references/lark-base-record-upsert.md) / [lark-base-record-batch-create.md](references/lark-base-record-batch-create.md) / [lark-base-record-batch-update.md](references/lark-base-record-batch-update.md) 和 [lark-base-cell-value.md](references/lark-base-cell-value.md) |
@@ -58,24 +86,34 @@ metadata:
| 表单题目创建/更新 | `+form-questions-create` / `+form-questions-update` | 读 [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) |
| 其他表单管理 | `+form-list/get/detail/create/update/delete` / `+form-questions-list/delete` | `+form-detail` 读 [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` |
-| 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 与启停状态 |
-| 高级权限与角色 | `+advperm-*` / `+role-*` | 角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md);角色 create/update 或解读完整配置再读权限 JSON SSOT [role-config.md](references/role-config.md);系统角色不可删除;关闭高级权限会影响自定义角色 |
+| Workflow | `+workflow-*` | 先读入口 [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md):它包含查询/启停/创建/修改的最短路径和常见 step 组合;只有创建/更新复杂 steps 时才继续读 schema 小文件;list/get/enable/disable 不读 schema |
+| 高级权限与角色 | `+advperm-*` / `+role-*` | 先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md)(含安全边界);权限 JSON 再读 [role-config.md](references/role-config.md) |
-## Base 心智模型
+## 注意事项
-- Base 曾用名 Bitable;返回字段、错误或旧文档里的 `bitable` 多为历史兼容,不代表应改走裸 API 或另一套命令。
-- `+base-block-list` 是查看一个 Base 内资源目录的新入口:它列出这个 Base 直接管理的 `folder/table/docx/dashboard/workflow`,适合先判断 Base 里有什么,再决定走 table、dashboard、workflow 或 docx 命令。
-- `base-block` 只负责资源目录管理,包括创建资源、移动到 folder、重命名和删除;具体资源内容仍走 table/dashboard/workflow 命令。
-- 新建 Base 时,强烈推荐一次性执行 `lark-cli base +base-create --name "" --table-name "" --fields ''`,同时配置新 Base 里唯一一个初始数据表的 name 和 schema;使用 `--fields` 前先读 [lark-base-field-json.md](references/lark-base-field-json.md) 或复用 `+field-create` 的字段 JSON 形状,不要猜字段属性。
-- `+base-create` 不传 `--table-name` 和 `--fields` 时,会创建一个默认 schema 的初始数据表。
-- 表、字段、视图、workflow、dashboard block 的名称和 ID 必须来自真实返回,不要凭用户口述猜。
-- 存储字段可写;系统字段、`formula`、`lookup` 只读;附件字段走专用 attachment 命令。
-- 一次性原始记录查询优先用 `+record-list` / `+record-search` 的 filter/sort;聚合分析优先用 `+data-query`;需要长期显示在表中时,才新增 `formula` / `lookup` 字段。
-- `formula` 适合常规计算、条件判断、文本/日期处理和长期派生指标;`lookup` 适合明确的跨表查找、筛选后取值或聚合引用。
-- 写入、分析、公式、lookup、workflow、dashboard 前,先读取真实结构:表、字段、视图、关联表和 dashboard block 名称都以命令返回为准。
-- 跨表场景必须读取目标表结构;link 单元格中的关联 `record_id` 只是连接键,最终回答要回查并展示用户可读字段。
+### 批量执行
-## 身份与权限降级
+能批量的操作尽量批量,不要一轮对话只处理一个对象。
+
+- 优先用原生批量能力:多表字段 `+field-list-batch`;批量写记录 `+record-batch-create` / `+record-batch-update`;部分命令参数本身支持多值(如 `+record-delete --record-id` 可重复传、`+record-share-link-create --record-ids`),先看 `--help`。
+- 没有原生批量命令时,对多个对象做同类操作在**一条 Bash 命令**里用 shell 循环完成。
+- 只读命令可用 `--jq` 收窄输出,避免无关字段灌入上下文。脚本输出只打印计数、ID 和失败项,不要回显完整 payload 或原始返回
+
+示例——一次取多个视图的配置:
+
+```bash
+for v in vewAAA vewBBB vewCCC; do
+ echo "== $v"
+ lark-cli base +view-get --base-token --table-id --view-id "$v" --as user
+done
+```
+
+### 善用 help
+
+- 参数不确定、要构造复杂 JSON、或命令带批量/隐藏选项时,先看对应reference或 `--help`,不要猜参数名或 JSON 结构;`+table-list` / `+base-create` 这类参数显而易见的简单命令直接执行,报参数错误再查 help,不要为它单花一轮。
+- 需要看多个命令的 help 时,合并在一条 Bash 命令里一次看完。
+
+### 身份与权限降级
- 默认显式使用 `--as user` 操作用户资源;只有用户明确要求应用身份时,才直接用 `--as bot`。
- user 身份报 scope/授权不足,或错误中包含 `permission_violations` / `hint`,先转 `lark-shared` 做用户授权恢复,不要直接降级 bot。
@@ -83,35 +121,27 @@ metadata:
- `91403` 或明确不可访问错误不要循环换身份重试。
- `+base-create` / `+base-copy` 若用 bot 身份执行,关注返回中的 `permission_grant`,并把用户是否可打开新 Base 告知用户。
-## 查询与统计规则
+### 查询与统计
-涉及查询、统计或判断结论时,先阅读 [lark-base-data-analysis-sop.md](references/lark-base-data-analysis-sop.md),并遵守:
+- 涉及筛选、排序、Top/Bottom N、聚合、分组、多表关联或任何全局结论时,先读 [lark-base-data-analysis-sop.md](references/lark-base-data-analysis-sop.md) 并按其 Hard Rules 执行。
+- 两条红线随时生效:能由 Base 云端表达的筛选/排序/聚合不要拉原始记录到本地手工处理;`has_more=true` 等分页信号未消除前,不能基于当前页下全局结论。
-1. `+record-list` 的默认页、固定 `--limit` 和本地 `jq` 只能证明已读取范围内的事实,不能直接支撑全局最值、全量计数、Top/Bottom N、异常识别或分组结论。
-2. 能由 Base 表达的筛选、排序、投影、聚合、分组和限制,应在 Base 云端查询能力中执行;不要先拉原始记录到本地上下文再手工筛选排序。
-3. `has_more=true` 或等价分页信号表示当前结果不是全量;除非用户只要样例/前 N 条,不能基于该页回答全局问题。
-4. 多表查询必须先确认关系字段和连接键;link 单元格里的 `record_id` 是关系键,不是用户可读答案。
-5. 最终答案必须能追溯到真实表、真实字段、查询范围、筛选/排序/聚合条件和必要的连接键。
-6. 一次性原始记录查询优先用 `+record-list` / `+record-search` 的 filter/sort;聚合分析优先用 `+data-query`;要把结果长期显示在表里,才考虑新增 `formula` / `lookup` 字段。
-7. `+data-query` 可返回聚合结果或维度字段行,但维度行按字段组合去重且不返回 `record_id`;需要逐条记录、记录定位或完整行级字段时,再用 `+record-list` / `+record-search` / `+record-get` 回查。
+### 写入前置
-## 写入前置规则
+- 写记录/字段前先读真实结构;表名、字段名、视图名必须来自真实返回,跨表场景还要读目标表结构。
+- 复杂 JSON 按快速路由读对应 reference:字段读 [lark-base-field-json.md](references/lark-base-field-json.md),记录读 [lark-base-cell-value.md](references/lark-base-cell-value.md)(写入红线:只写存储字段、批量上限、并发冲突等,见其顶层规则)。
+- 删除、角色更新、字段更新等高风险操作遵循 CLI 的 confirmation gate;目标不明确先用 get/list 消歧;workflow/role 等复杂写操作创建后用 get 回读确认,必要时先 `--dry-run` 预演。
-- 写记录前先读字段结构;只写存储字段。系统字段、附件字段、`formula`、`lookup` 不作为普通记录写入目标。
-- 附件上传、下载、删除走专用 `+record-*-attachment` 命令。
-- 写字段前先读 [lark-base-field-json.md](references/lark-base-field-json.md);涉及 `formula` / `lookup` 时必须读 [formula-field-guide.md](references/formula-field-guide.md) / [lookup-field-guide.md](references/lookup-field-guide.md)。
-- 表名、字段名、视图名、workflow 配置中的名称必须来自真实返回;跨表场景还要读取目标表结构。
-- 删除、角色更新、字段更新等高风险操作遵循 CLI 的 confirmation gate;目标不明确时先用 get/list 消歧。
-- 批量写入单批最多 200 条;连续写同一表时串行执行,遇到 `1254291` 按短暂等待后重试处理。
-- `+record-batch-update` 是“同值批量更新”:同一份 patch 应用到全部 `record_id_list`,不要拿它做逐行不同值映射。
-- select/multiselect 写入未知选项可能触发平台新增选项;不是要新增时,先用 `+field-list` 或 `+field-search-options` 确认可选值。
+### 表单与视图
-## 表单与视图细节
+- `+form-submit` 前必须先 `+form-detail`;提交规则(filter 隐藏题不填、附件写在 `attachments` 并带 `--base-token`)见 [lark-base-form-submit.md](references/lark-base-form-submit.md)。
+- 视图配置先用对应 get 命令读现状,只替换要变更的部分;一次性筛选/排序先用 `+record-list` / `+record-search` 验证,再按需沉淀为持久视图。
-- `+form-submit` 前必须先跑 `+form-detail`,读取 `questions[].type`、`required`、`filter` 和附件场景需要的 `base_token`;不要填写被 filter 隐藏的问题。
-- 表单附件不要写进 `fields`,放在 `--json.attachments`;提交附件时必须同时传表单所属 Base 的 `--base-token`。
-- `+view-set-filter` 是唯一保留的 view reference;sort/group/card/timebar/visible-fields 这类配置先用对应 get 命令读现状,保留未修改字段,只替换用户要求变更的配置。
-- 视图适合持久化、共享和 UI 复用;一次性筛选/排序可先用 `+record-list` / `+record-search` 的 filter/sort 验证结果,再按需要沉淀为持久视图。
+### Dashboard / Workflow / Role
+
+- Dashboard 的复杂点是 block 的 `data_config`:创建/更新 block 前读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md),组件串行创建;布局/换图表类型/删除具名图表等操作要点见 [lark-base-dashboard.md](references/lark-base-dashboard.md) 的「执行要点」。`+dashboard-block-get-data` 只返回图表数据,元数据用 `+dashboard-block-get`。
+- Workflow 的复杂点是 `steps`:先读入口 [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md),用其中的最短路径和场景表完成查询/启停/常见创建修改;需要具体 step 字段再按需读 schema 小文件;创建后 `+workflow-get` 回读验证。
+- Role 的复杂点是权限 JSON:先读 [lark-base-role-guide.md](references/lark-base-role-guide.md)(含安全边界),权限 JSON SSOT 读 [role-config.md](references/role-config.md);删除角色、关闭高级权限前确认目标和影响。
## Token 与链接
@@ -122,19 +152,10 @@ metadata:
| `/base/{token}?table={id}` | `table` 参数用于定位 Base 内对象:`tbl` 开头是数据表 `--table-id`;`blk` 开头是 dashboard ID;`wkf` 开头是 workflow ID |
| `/base/{token}?view={id}` | `view` 参数用于定位表视图,提取为 `--view-id`;通常还需要确认 `table` 参数或先查表结构 |
| `/share/base/form/{shareToken}` | 表单分享链接;这是表单 share token,走 `+form-detail` / `+form-submit --share-token ` |
-| `/share/base/view/{shareToken}` | 视图分享链接;具有分享权限语义,暂不支持用 CLI 直接访问,引导用户在浏览器或飞书客户端打开 |
-| `/share/base/dashboard/{shareToken}` | 仪表盘分享链接;具有分享权限语义,暂不支持用 CLI 直接访问,引导用户在浏览器或飞书客户端打开 |
-| `/record/{shareToken}` | 记录分享链接;暂不支持用 CLI 直接访问,引导用户在浏览器或飞书客户端打开。若用户想生成现有记录的分享链接,用 `+record-share-link-create --base-token --table-id --record-ids ` |
-| `/base/workspace/{token}` | BaseApp / workspace 链接;暂不支持用 CLI 直接访问 |
+| `/share/base/view/...` / `/share/base/dashboard/...` / `/record/...` / `/base/workspace/...` | 分享链接与 workspace 链接,暂不支持用 CLI 直接访问,引导用户在飞书客户端打开;要生成记录分享链接用 `+record-share-link-create` |
`wiki +node-get` 返回非 `bitable` 时,不继续使用 Base 命令:`docx` 转文档,`sheet` 转表格,其他云空间对象转对应 skill 或 drive。
-## Dashboard / Workflow / Role
-
-- Dashboard 的复杂点是 block 的 `data_config`,不是 list/get/create/delete 命令参数。创建或更新 block 前先读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md),组件必须串行创建;`+dashboard-arrange` 是服务端智能布局,只在用户明确要求重排/美化时执行。`+dashboard-block-get-data` 读取图表最终计算结果,不返回 block 名称、类型、布局或 `data_config`;需要元数据先用 `+dashboard-block-get`。
-- Workflow 的复杂点是 `steps` 结构。创建、更新或解释完整 workflow 时读入口 [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);enable/disable/list 只需确认 workflow ID、当前启停状态和用户意图。
-- Role 的复杂点是权限 JSON。角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md);`+role-create` 只支持自定义角色;`+role-update` 是 delta merge;角色 create/update 或解读完整配置时读权限 JSON SSOT [role-config.md](references/role-config.md)。`+role-delete` 只适用于自定义角色,系统角色不可删除;删除角色和关闭高级权限前必须确认目标和影响。
-
## 常见恢复
| 错误 / 现象 | 恢复动作 |
@@ -149,18 +170,3 @@ metadata:
| `1254104` | 批量超过 200,分批调用 |
| `1254291` | 并发写冲突,串行写入并在批次间短暂等待 |
| `91403` | 无权限访问该 Base,按 `lark-shared` 权限流程处理,不要盲目重试 |
-
-## 保留 Reference
-
-- [lark-base-data-analysis-sop.md](references/lark-base-data-analysis-sop.md):查询/统计/全局结论的选路 SOP
-- [lark-base-data-query-guide.md](references/lark-base-data-query-guide.md) / [lark-base-data-query.md](references/lark-base-data-query.md):聚合查询入口 fewshot 与 DSL SSOT
-- [lark-base-cell-value.md](references/lark-base-cell-value.md):记录 CellValue 构造
-- [lark-base-field-json.md](references/lark-base-field-json.md):字段 JSON 构造
-- [formula-field-guide.md](references/formula-field-guide.md) / [lookup-field-guide.md](references/lookup-field-guide.md):公式与 lookup 字段
-- [lark-base-field-create.md](references/lark-base-field-create.md) / [lark-base-field-update.md](references/lark-base-field-update.md):字段创建/更新命令级补充
-- [lark-base-record-upsert.md](references/lark-base-record-upsert.md) / [lark-base-record-batch-create.md](references/lark-base-record-batch-create.md) / [lark-base-record-batch-update.md](references/lark-base-record-batch-update.md) / [lark-base-record-history-list.md](references/lark-base-record-history-list.md):记录写入 JSON 与历史返回解释
-- [lark-base-view-set-filter.md](references/lark-base-view-set-filter.md):视图筛选 JSON
-- [lark-base-form-detail.md](references/lark-base-form-detail.md) / [lark-base-form-submit.md](references/lark-base-form-submit.md) / [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):表单详情、提交和复杂 JSON
-- [lark-base-dashboard.md](references/lark-base-dashboard.md) / [dashboard-block-data-config.md](references/dashboard-block-data-config.md) / [lark-base-dashboard-block-get-data.md](references/lark-base-dashboard-block-get-data.md):仪表盘、组件配置与图表结果协议
-- [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md) / [lark-base-workflow-schema.md](references/lark-base-workflow-schema.md):workflow 入口与 steps JSON SSOT
-- [lark-base-role-guide.md](references/lark-base-role-guide.md) / [role-config.md](references/role-config.md):角色入口与权限 JSON SSOT
diff --git a/skills/lark-base/references/dashboard-block-data-config.md b/skills/lark-base/references/dashboard-block-data-config.md
index 92e27478..ece68035 100644
--- a/skills/lark-base/references/dashboard-block-data-config.md
+++ b/skills/lark-base/references/dashboard-block-data-config.md
@@ -2,6 +2,8 @@
Block 的 `data_config` 字段因 `type` 不同而变化。本文档是 dashboard block `data_config` 的单一事实来源(SSOT),包含组件类型、字段结构、筛选格式、约束和可复制模板。
+`data_config` 是 dashboard block 的数据源配置。先用 `+table-list` 拿表,再用 `+field-list-batch --table-id <表1> --table-id <表2>` **一次批量**拿到相关表字段(不要逐表多次 `+field-list`,每次多余调用都拉高 token);表用 **name**,不是 table_id;字段用 **field_name**。
+
## 支持的组件类型(`type` 枚举)
| type 值 | 说明 |
diff --git a/skills/lark-base/references/formula-examples.md b/skills/lark-base/references/formula-examples.md
new file mode 100644
index 00000000..508b799a
--- /dev/null
+++ b/skills/lark-base/references/formula-examples.md
@@ -0,0 +1,115 @@
+# Base Formula Examples and Requirement Translation
+
+> 本文件是 [formula-field-guide.md](formula-field-guide.md) 的按需补充:完整示例与"自然语言需求 → 公式"的翻译规则。
+
+## Section 13: Complete Examples
+
+### Example 1: Employee sales summary
+
+**Table structure** (from `+table-get`):
+
+- Employees: EmployeeID (Text), Name (Text), Department (Text)
+- Sales: ContractID (Number), SalespersonID (Text), Quantity (Number), Total (Number)
+
+**Current table**: Employees
+
+**Requirement**: For each employee, output "Sold XX orders" if they have sales records, otherwise "No sales records".
+
+**Formula**:
+
+```
+IF(
+ [Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) >= 1,
+ "Sold " & [Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) & " orders",
+ "No sales records"
+)
+```
+
+**Field JSON**:
+
+```json
+{
+ "type": "formula",
+ "name": "Sales Summary",
+ "expression": "IF([Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) >= 1, \"Sold \" & [Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) & \" orders\", \"No sales records\")"
+}
+```
+
+**Explanation**: `[Sales].COUNTIF(...)` uses the entire Sales table as data range. CurrentValue represents each row in Sales, accessing `CurrentValue.[SalespersonID]` for that row's salesperson. `[EmployeeID]` refers to the current row in the Employees table (where the formula lives).
+
+### Example 2: Chained cross-table access via link fields
+
+**Table structure**:
+
+- Orders: ID (`auto_number`), OrderItems (`link` [target: OrderItems, foreign key: ID])
+- OrderItems: ID (`auto_number`), Product (`link` [target: Products, foreign key: ID])
+- Products: ID (`auto_number`), ProductName (`text`)
+
+**Current table**: Orders
+
+**Requirement**: Deduplicate and comma-join all product names from linked order items.
+
+**Formula**:
+
+```
+[OrderItems].[Product].[ProductName].UNIQUE().ARRAYJOIN(",")
+```
+
+**Field JSON**:
+
+```json
+{
+ "type": "formula",
+ "name": "Product List",
+ "expression": "[OrderItems].[Product].[ProductName].UNIQUE().ARRAYJOIN(\",\")"
+}
+```
+
+**Explanation**: `[OrderItems]` gets linked order item records, `.[Product]` expands to each item's linked product, `.[ProductName]` gets all product names, `.UNIQUE()` deduplicates, `.ARRAYJOIN(",")` joins with commas.
+
+### Example 3: Cross-table filter + sort
+
+**Table structure**:
+
+- Projects: ProjectName (Text), Status (Text), Owner (Text)
+- Tasks: TaskName (Text), Project (Text), Priority (Number), DueDate (Date)
+
+**Current table**: Projects
+
+**Requirement**: Find the highest-priority (lowest number) task name for the current project.
+
+**Formula**:
+
+```
+FIRST(
+ [Tasks].FILTER(CurrentValue.[Project] = [ProjectName]).SORTBY([Tasks].[Priority], TRUE).[TaskName]
+)
+```
+
+**Field JSON**:
+
+```json
+{
+ "type": "formula",
+ "name": "Top Priority Task",
+ "expression": "FIRST([Tasks].FILTER(CurrentValue.[Project] = [ProjectName]).SORTBY([Tasks].[Priority], TRUE).[TaskName])"
+}
+```
+
+**Explanation**: `[Tasks].FILTER(CurrentValue.[Project] = [ProjectName])` filters tasks belonging to the current project. `.SORTBY([Tasks].[Priority], TRUE)` sorts by priority ascending. `.[TaskName]` extracts task names. `FIRST(...)` gets the first one (highest priority).
+
+---
+
+## Section 14: Translating User Requirements to Formulas
+
+When the user describes their formula need in natural language, follow these rules to convert it into a precise expression:
+
+1. **Numbers must use precise values**: "less than 80%" → field value less than `0.8`. "above 1000" → `>= 1000`.
+2. **Interval boundaries**: "above/below/within" = closed (inclusive); "less than/more than/outside" = open (exclusive).
+3. **Branching logic** must be organized as an ordered list with a fallback branch. Each branch has a condition and output.
+ - Example: "return risk level for 1-3" → `IFS([Value] = 1, "low", [Value] = 2, "medium", [Value] = 3, "high")` with an `IFERROR` or trailing empty-string fallback.
+4. **Multi-level branches must be flattened** to a single level. Nested if-else chains → flat IFS.
+5. **Branch conditions must be mutually exclusive**. If the user's conditions overlap, rewrite to eliminate ambiguity.
+6. **Reorder branches by logical priority** if the user's order is illogical (e.g., check specific conditions before catch-all).
+
+---
diff --git a/skills/lark-base/references/formula-field-guide.md b/skills/lark-base/references/formula-field-guide.md
index 5d9a4635..c430b964 100644
--- a/skills/lark-base/references/formula-field-guide.md
+++ b/skills/lark-base/references/formula-field-guide.md
@@ -34,11 +34,11 @@ When creating a formula field, the Agent should:
This is the foundation of formula logic. You must determine this before writing any formula.
-| Syntax | Meaning | Return type | Example |
-| --------------------- | -------------------------------------------- | ---------------------- | -------------------------------------------- |
-| `[Field]` | Value of this field in the current row | Scalar (single value) | `[Name]` → `"Alice"` |
+| Syntax | Meaning | Return type | Example |
+|---|---|---|---|
+| `[Field]` | Value of this field in the current row | Scalar (single value) | `[Name]` → `"Alice"` |
| `[TableName].[Field]` | All values of this field in the target table | List (multiple values) | `[Employees].[Name]` → `["Alice","Bob",...]` |
-| `[TableName]` | The target table (entire table) | Table reference | Used as data range for FILTER/COUNTIF etc. |
+| `[TableName]` | The target table (entire table) | Table reference | Used as data range for FILTER/COUNTIF etc. |
**Rules**:
@@ -59,7 +59,7 @@ This is the foundation of formula logic. You must determine this before writing
### Field storage types
| Type | Description | Supported operations |
-|------|-------------|----------------------|
+|---|---|---|
| `number` | Stored as numeric value | Math operations, comparisons, auto-converts to string for concatenation |
| `text` | Stored as string | String operations; can participate in math if content is numeric, otherwise errors |
| `datetime` | Date object | Date functions, add/subtract with numbers; auto-converts to default format string when using `&` — use TEXT to format first for controlled output |
@@ -69,13 +69,13 @@ This is the foundation of formula logic. You must determine this before writing
### Implicit type conversion
-| Scenario | Conversion rule |
-| ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
-| Number + Float | → Float |
-| Date + Number | → Date (adds/subtracts days). Use `+`/`-` for whole days, use `DURATION()` for hour/minute/second precision |
-| Date - Date | → Duration |
-| Boolean compared with Number | Boolean auto-converts to number (TRUE=1, FALSE=0) |
-| `&` concatenation | Both sides auto-convert to string |
+| Scenario | Conversion rule |
+|---|---|
+| Number + Float | → Float |
+| Date + Number | → Date (adds/subtracts days). Use `+`/`-` for whole days, use `DURATION()` for hour/minute/second precision |
+| Date - Date | → Duration |
+| Boolean compared with Number | Boolean auto-converts to number (TRUE=1, FALSE=0) |
+| `&` concatenation | Both sides auto-convert to string |
### Type consistency in comparisons
@@ -97,12 +97,12 @@ When using comparison operators (`>`, `>=`, `<`, `<=`, `=`, `!=`), **both sides
### CurrentValue meaning in different contexts
-| Data range type | CurrentValue represents | Access pattern | Example |
-| ---------------------------- | ----------------------- | --------------------------- | --------------------------------------------------------- |
-| Entire table `[TableName]` | A row in the table | `CurrentValue.[FieldName]` | `[Orders].FILTER(CurrentValue.[Amount] > 100).[Customer]` |
-| Column `[TableName].[Field]` | A single field value | Use `CurrentValue` directly | `[Orders].[Amount].FILTER(CurrentValue > 100)` |
+| Data range type | CurrentValue represents | Access pattern | Example |
+|---|---|---|---|
+| Entire table `[TableName]` | A row in the table | `CurrentValue.[FieldName]` | `[Orders].FILTER(CurrentValue.[Amount] > 100).[Customer]` |
+| Column `[TableName].[Field]` | A single field value | Use `CurrentValue` directly | `[Orders].[Amount].FILTER(CurrentValue > 100)` |
| `select` (`multiple=true`) field `[Tags]` | One option | Use `CurrentValue` directly | `[Tags].FILTER(CurrentValue = "Important")` |
-| LIST-generated list | One element | Use `CurrentValue` directly | `LIST(1,2,3).MAP(CurrentValue * 2)` |
+| LIST-generated list | One element | Use `CurrentValue` directly | `LIST(1,2,3).MAP(CurrentValue * 2)` |
### Key rules
@@ -113,11 +113,11 @@ When using comparison operators (`>`, `>=`, `<`, `<=`, `=`, `!=`), **both sides
### Anti-patterns
-| Wrong | Reason | Correct |
-| ---------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------ |
-| `[Table].[Col].FILTER(CurrentValue.[Col] > 0)` | Data range is a column; CurrentValue is a scalar, cannot use `.` to access fields | `[Table].[Col].FILTER(CurrentValue > 0)` |
-| `[Table].FILTER(CurrentValue > 100)` | Data range is a table; CurrentValue is a row, cannot compare directly | `[Table].FILTER(CurrentValue.[Amount] > 100).[Amount]` |
-| `CurrentValue + 1` (at top level) | CurrentValue can only be used inside iteration functions | Use inside MAP/FILTER etc. |
+| Wrong | Reason | Correct |
+|---|---|---|
+| `[Table].[Col].FILTER(CurrentValue.[Col] > 0)` | Data range is a column; CurrentValue is a scalar, cannot use `.` to access fields | `[Table].[Col].FILTER(CurrentValue > 0)` |
+| `[Table].FILTER(CurrentValue > 100)` | Data range is a table; CurrentValue is a row, cannot compare directly | `[Table].FILTER(CurrentValue.[Amount] > 100).[Amount]` |
+| `CurrentValue + 1` (at top level) | CurrentValue can only be used inside iteration functions | Use inside MAP/FILTER etc. |
---
@@ -125,12 +125,12 @@ When using comparison operators (`>`, `>=`, `<`, `<=`, `=`, `!=`), **both sides
Base formulas **only allow** the following operators. `like`, `in`, `<>`, `**`, `^` etc. are prohibited.
-| Category | Operators | Description |
-| ------------- | -------------------------- | -------------------------------------------------------------------------- |
-| Arithmetic | `+` `-` `*` `/` `%` | Add, subtract, multiply, divide, modulo (`%` is equivalent to `MOD()`) |
-| Comparison | `>` `>=` `<` `<=` `=` `!=` | Greater than, greater or equal, less than, less or equal, equal, not equal |
-| Logical | `&&` `\|\|` | AND, OR |
-| Concatenation | `&` | Text concatenation; non-text values auto-convert to string |
+| Category | Operators | Description |
+|---|---|---|
+| Arithmetic | `+` `-` `*` `/` `%` | Add, subtract, multiply, divide, modulo (`%` is equivalent to `MOD()`) |
+| Comparison | `>` `>=` `<` `<=` `=` `!=` | Greater than, greater or equal, less than, less or equal, equal, not equal |
+| Logical | `&&` `\|\|` | AND, OR |
+| Concatenation | `&` | Text concatenation; non-text values auto-convert to string |
**Important**:
@@ -174,10 +174,10 @@ Retrieves the target field values for all linked records as a list. Supports con
### Two calling styles
-| Style | Format | Description |
-| ---------- | ------------------ | ----------------------------------- |
-| Functional | `FUNC(arg1, arg2)` | Works for all functions |
-| Chained | `arg1.FUNC(arg2)` | Moves the first argument before `.` |
+| Style | Format | Description |
+|---|---|---|
+| Functional | `FUNC(arg1, arg2)` | Works for all functions |
+| Chained | `arg1.FUNC(arg2)` | Moves the first argument before `.` |
**Rules**:
@@ -228,175 +228,139 @@ After the result column, it's recommended to flatten with `.LISTCOMBINE()` first
---
-## Section 8: Complete Function Reference
+## Section 8: Function Reference (common functions)
+
+> 本表覆盖常用函数(含评测与真实负载中 100% 出现过的函数)。三角/双曲/随机数/进制转换等罕见函数的签名在 [formula-functions-extended.md](formula-functions-extended.md),仅当用户明确要求这些函数时再读。
### 8.1 Logic functions
-| Function | Signature | Return type | Description |
-| ------------- | ------------------------------------------------------------------ | -------------------- | -------------------------------------------------------------------------------------------- |
-| IF | `IF(condition, true_val, [false_val])` | Matches branch type | Returns true_val when TRUE, false_val otherwise; omitting false_val returns false (not null) |
-| IFS | `IFS(cond1, val1, cond2, val2, ...)` | Matches branch type | Multi-condition branching; returns value for the first TRUE condition |
-| SWITCH | `SWITCH(expr, match1, result1, [match2, result2, ...], [default])` | Matches branch type | Matches expression value and returns corresponding result |
-| IFERROR | `IFERROR(expr, fallback)` | Matches branch type | Returns fallback when expression errors |
-| IFBLANK | `IFBLANK(expr, fallback)` | Matches branch type | Returns fallback when expression is blank (blank = NULL/empty string/empty list) |
-| AND | `AND(cond1, cond2, ...)` | Boolean | TRUE when all conditions are TRUE |
-| OR | `OR(cond1, cond2, ...)` | Boolean | TRUE when any condition is TRUE |
-| NOT | `NOT(condition)` | Boolean | Logical negation |
-| ISBLANK | `ISBLANK(value)` | Boolean | Tests if blank (NULL/empty string/empty list are blank; 0 and FALSE are not) |
-| ISNULL | `ISNULL(value)` | Boolean | Tests if NULL (only NULL is true; empty string is not) |
-| ISERROR | `ISERROR(expr)` | Boolean | Tests if expression errors |
-| ISNUMBER | `ISNUMBER(value)` | Boolean | Tests if value is a number |
-| CONTAIN | `CONTAIN(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains the value; **does NOT do text substring matching** |
-| CONTAINSALL | `CONTAINSALL(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains all specified values |
-| CONTAINSONLY | `CONTAINSONLY(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains only the specified values |
-| TRUE | `TRUE()` | Boolean | Returns TRUE |
-| FALSE | `FALSE()` | Boolean | Returns FALSE |
-| RECORD_ID | `RECORD_ID()` | Text | Returns the current row's record ID |
-| RANDOMBETWEEN | `RANDOMBETWEEN(min_int, max_int, [keep_updating])` | Number | Random integer in the specified range |
-| RANDOMITEM | `RANDOMITEM(list, [keep_updating])` | Matches element type | Randomly picks one element from a list |
+| Function | Signature | Return type | Description |
+|---|---|---|---|
+| IF | `IF(condition, true_val, [false_val])` | Matches branch type | Returns true_val when TRUE, false_val otherwise; omitting false_val returns false (not null) |
+| IFS | `IFS(cond1, val1, cond2, val2, ...)` | Matches branch type | Multi-condition branching; returns value for the first TRUE condition |
+| SWITCH | `SWITCH(expr, match1, result1, [match2, result2, ...], [default])` | Matches branch type | Matches expression value and returns corresponding result |
+| IFERROR | `IFERROR(expr, fallback)` | Matches branch type | Returns fallback when expression errors |
+| IFBLANK | `IFBLANK(expr, fallback)` | Matches branch type | Returns fallback when expression is blank (blank = NULL/empty string/empty list) |
+| AND | `AND(cond1, cond2, ...)` | Boolean | TRUE when all conditions are TRUE |
+| OR | `OR(cond1, cond2, ...)` | Boolean | TRUE when any condition is TRUE |
+| NOT | `NOT(condition)` | Boolean | Logical negation |
+| ISBLANK | `ISBLANK(value)` | Boolean | Tests if blank (NULL/empty string/empty list are blank; 0 and FALSE are not) |
+| ISNULL | `ISNULL(value)` | Boolean | Tests if NULL (only NULL is true; empty string is not) |
+| CONTAIN | `CONTAIN(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains the value; **does NOT do text substring matching** |
+| RECORD_ID | `RECORD_ID()` | Text | Returns the current row's record ID |
### 8.2 Numeric functions
-| Function | Signature | Return type | Description |
-| --- | --- | --- | --- |
-| SUM | `SUM(val1, val2, ...)` | Number | Sum; accepts multiple values or a list |
-| AVERAGE | `AVERAGE(val1, val2, ...)` | Number | Average |
-| MAX | `MAX(val1, val2, ...)` | Number | Maximum |
-| MIN | `MIN(val1, val2, ...)` | Number | Minimum |
-| MEDIAN | `MEDIAN(val1, val2, ...)` | Number | Median |
-| COUNTA | `COUNTA(val1, val2, ...)` | Number | Count of non-blank values |
-| COUNTIF | `COUNTIF(data_range, condition)` | Number | Count matching items. Data range can be a **table** (CurrentValue is a row, use `CurrentValue.[Field]`) or a **column** (CurrentValue is a scalar value) |
-| SUMIF | `SUMIF(data_range, condition)` | Number | Sum matching values. Data range **must be a numeric column** (e.g. `[Table].[NumField]`); CurrentValue is each value in that column (scalar), cannot use `CurrentValue.[Field]` to access other fields. For cross-field conditions, use FILTER+SUM instead |
-| ROUND | `ROUND(number, digits)` | Number | Round. digits: 1=one decimal, 0=integer, -1=tens place |
-| ROUNDUP | `ROUNDUP(number, digits)` | Number | Round away from zero. Same digits semantics as ROUND |
-| ROUNDDOWN | `ROUNDDOWN(number, digits)` | Number | Round toward zero. Same digits semantics as ROUND |
-| FLOOR | `FLOOR(number, [base])` | Number | Round down to nearest multiple of base (default 1) |
-| CEILING | `CEILING(number, [base])` | Number | Round up to nearest multiple of base (default 1) |
-| ABS | `ABS(number)` | Number | Absolute value |
-| INT | `INT(number)` | Integer | Truncate to integer |
-| MOD | `MOD(dividend, divisor)` | Number | Modulo |
-| POWER | `POWER(base, exponent)` | Number | Exponentiation |
-| QUOTIENT | `QUOTIENT(dividend, divisor)` | Number | Integer division |
-| VALUE | `VALUE(text)` | Number | Convert text to number |
-| ISODD | `ISODD(number)` | Boolean | Tests if number is odd |
-| RANK | `RANK(value, search_range, [ascending])` | Number | Rank of value in range; default descending |
-| SEQUENCE | `SEQUENCE(start, end, [step])` | List | Generate number sequence |
-| PI | `PI()` | Number | Pi constant |
-| SIN/COS/TAN/ASIN/ACOS/ATAN/ATAN2/SINH/COSH/TANH/ASINH/ACOSH/ATANH | `func(radians_or_value)` | Number | Trigonometric and hyperbolic functions; arguments in radians |
+| Function | Signature | Return type | Description |
+|---|---|---|---|
+| SUM | `SUM(val1, val2, ...)` | Number | Sum; accepts multiple values or a list |
+| AVERAGE | `AVERAGE(val1, val2, ...)` | Number | Average |
+| MAX | `MAX(val1, val2, ...)` | Number | Maximum |
+| MIN | `MIN(val1, val2, ...)` | Number | Minimum |
+| COUNTA | `COUNTA(val1, val2, ...)` | Number | Count of non-blank values |
+| COUNTIF | `COUNTIF(data_range, condition)` | Number | Count matching items. Data range can be a **table** (CurrentValue is a row, use `CurrentValue.[Field]`) or a **column** (CurrentValue is a scalar value) |
+| SUMIF | `SUMIF(data_range, condition)` | Number | Sum matching values. Data range **must be a numeric column** (e.g. `[Table].[NumField]`); CurrentValue is each value in that column (scalar), cannot use `CurrentValue.[Field]` to access other fields. For cross-field conditions, use FILTER+SUM instead |
+| ROUND | `ROUND(number, digits)` | Number | Round. digits: 1=one decimal, 0=integer, -1=tens place |
+| ABS | `ABS(number)` | Number | Absolute value |
+| INT | `INT(number)` | Integer | Truncate to integer |
+| MOD | `MOD(dividend, divisor)` | Number | Modulo |
+| VALUE | `VALUE(text)` | Number | Convert text to number |
### 8.3 Text functions
-| Function | Signature | Return type | Description |
-| --------------- | ---------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------- |
-| CONCATENATE | `CONCATENATE(text1, text2, ...)` | Text | Concatenate multiple texts; supports lists as input |
-| LEN | `LEN(text)` | Number | Character count |
-| LEFT | `LEFT(text, [count])` | Text | Extract from left; default 1 |
-| RIGHT | `RIGHT(text, [count])` | Text | Extract from right; default 1 |
-| MID | `MID(text, start, count)` | Text | Extract from middle |
-| FIND | `FIND(search_val, search_range, [start])` | Number | Find substring position (case-sensitive); returns -1 if not found |
-| REPLACE | `REPLACE(text, start, count, new_text)` | Text | Replace by position |
-| SUBSTITUTE | `SUBSTITUTE(text, old_text, new_text, [occurrence])` | Text | Replace by content; can specify which occurrence |
-| UPPER | `UPPER(text)` | Text | Convert to uppercase |
-| LOWER | `LOWER(text)` | Text | Convert to lowercase |
-| TRIM | `TRIM(text)` | Text | Remove leading/trailing spaces |
-| TEXT | `TEXT(value, format)` | Text | Format output. Date formats: `"YYYY-MM-DD"`, `"YYYY/MM/DD hh:mm:ss"`; number formats: `"00"`, `"000.00"` |
-| CONTAINTEXT | `CONTAINTEXT(text, search_text)` | Boolean | Tests if text contains substring (text substring matching) |
-| SPLIT | `SPLIT(text, delimiter)` | List | Split text by delimiter |
-| TODATE | `TODATE(value)` | Date | Convert date string to date type |
-| CHAR | `CHAR(number)` | Text | ASCII code to character |
-| FORMAT | `FORMAT(template, [val1, val2, ...])` | Text | Template string formatting; use `{1}`, `{2}` as placeholders |
-| HYPERLINK | `HYPERLINK(url, [display_text])` | Hyperlink | Create a hyperlink |
-| ENCODEURL | `ENCODEURL(text)` | Text | URL encode |
-| REGEXMATCH | `REGEXMATCH(text, regex)` | Boolean | Regex match test |
-| REGEXEXTRACT | `REGEXEXTRACT(text, regex)` | List | Extract first match's capture groups |
-| REGEXEXTRACTALL | `REGEXEXTRACTALL(text, regex)` | 2D List | Extract all matches |
-| REGEXREPLACE | `REGEXREPLACE(text, regex, replacement)` | Text | Regex replace |
+| Function | Signature | Return type | Description |
+|---|---|---|---|
+| CONCATENATE | `CONCATENATE(text1, text2, ...)` | Text | Concatenate multiple texts; supports lists as input |
+| LEN | `LEN(text)` | Number | Character count |
+| LEFT | `LEFT(text, [count])` | Text | Extract from left; default 1 |
+| RIGHT | `RIGHT(text, [count])` | Text | Extract from right; default 1 |
+| MID | `MID(text, start, count)` | Text | Extract from middle |
+| REPLACE | `REPLACE(text, start, count, new_text)` | Text | Replace by position |
+| SUBSTITUTE | `SUBSTITUTE(text, old_text, new_text, [occurrence])` | Text | Replace by content; can specify which occurrence |
+| UPPER | `UPPER(text)` | Text | Convert to uppercase |
+| LOWER | `LOWER(text)` | Text | Convert to lowercase |
+| TRIM | `TRIM(text)` | Text | Remove leading/trailing spaces |
+| TEXT | `TEXT(value, format)` | Text | Format output. Date formats: `"YYYY-MM-DD"`, `"YYYY/MM/DD hh:mm:ss"`; number formats: `"00"`, `"000.00"` |
+| CONTAINTEXT | `CONTAINTEXT(text, search_text)` | Boolean | Tests if text contains substring (text substring matching) |
+| SPLIT | `SPLIT(text, delimiter)` | List | Split text by delimiter |
### 8.4 Date functions
-| Function | Signature | Return type | Description |
-| ----------- | ----------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------- |
-| NOW | `NOW()` | Date | Current date and time |
-| TODAY | `TODAY()` | Date | Current date (midnight) |
-| DATE | `DATE(year, month, day)` | Date | Construct a date |
-| YEAR | `YEAR(date)` | Number | Extract year |
-| MONTH | `MONTH(date)` | Number | Extract month |
-| DAY | `DAY(date)` | Number | Extract day |
-| HOUR | `HOUR(date)` | Number | Extract hour |
-| MINUTE | `MINUTE(date)` | Number | Extract minute |
-| SECOND | `SECOND(date)` | Number | Extract second |
-| WEEKDAY | `WEEKDAY(date, [type])` | Number | Day of week |
-| WEEKNUM | `WEEKNUM(date, [type])` | Number | Week number |
-| DAYS | `DAYS(end_date, start_date)` | Number | Days between two dates (end - start), includes decimals. **Note parameter order: end date comes first** |
-| DATEDIF | `DATEDIF(start_date, end_date, [unit])` | Number | Whole days/months/years between dates. Unit: `"D"`(default)/`"M"`/`"Y"`. **Start must be before end** |
-| DURATION | `DURATION(days, [hours], [minutes], [seconds])` | Duration | Create a duration for date arithmetic |
-| EDATE | `EDATE(date, months)` | Date | Date N months later |
-| EOMONTH | `EOMONTH(date, [months])` | Date | End of month N months later; months default 0 |
-| WORKDAY | `WORKDAY(start_date, days, [holidays])` | Date | Date N workdays later (skips weekends and holidays) |
-| NETWORKDAYS | `NETWORKDAYS(start_date, end_date, [holidays])` | Number | Workdays between dates (inclusive) |
+| Function | Signature | Return type | Description |
+|---|---|---|---|
+| NOW | `NOW()` | Date | Current date and time |
+| TODAY | `TODAY()` | Date | Current date (midnight) |
+| DATE | `DATE(year, month, day)` | Date | Construct a date |
+| YEAR | `YEAR(date)` | Number | Extract year |
+| MONTH | `MONTH(date)` | Number | Extract month |
+| DAY | `DAY(date)` | Number | Extract day |
+| HOUR | `HOUR(date)` | Number | Extract hour |
+| MINUTE | `MINUTE(date)` | Number | Extract minute |
+| SECOND | `SECOND(date)` | Number | Extract second |
+| WEEKDAY | `WEEKDAY(date, [type])` | Number | Day of week |
+| WEEKNUM | `WEEKNUM(date, [type])` | Number | Week number |
+| DAYS | `DAYS(end_date, start_date)` | Number | Days between two dates (end - start), includes decimals. **Note parameter order: end date comes first** |
+| DATEDIF | `DATEDIF(start_date, end_date, [unit])` | Number | Whole days/months/years between dates. Unit: `"D"`(default)/`"M"`/`"Y"`. **Start must be before end** |
+| NETWORKDAYS | `NETWORKDAYS(start_date, end_date, [holidays])` | Number | Workdays between dates (inclusive) |
### 8.5 List functions
-| Function | Signature | Return type | Description |
-| --- | --- | --- | --- |
-| LIST | `LIST(val1, val2, ...)` | List | Create a list |
-| FIRST | `FIRST(list)` | Scalar | First element |
-| LAST | `LAST(list)` | Scalar | Last element |
-| NTH | `NTH(list, index)` | Scalar | Nth element (1-based) |
-| FILTER | `[Table].FILTER(condition).[ResultCol]` or `[Table].[Col].FILTER(condition)` | List | Filter by condition. When data range is a table, result column is **required**; when it's a column/list, it's not needed. Use CurrentValue in conditions. Add `.LISTCOMBINE()` when result column is multi-value |
-| MAP | `data_range.MAP(mapping_expr)` | List | Apply mapping to each element. Use CurrentValue in mapping |
-| SORT | `SORT(list, [ascending])` | List | Sort; default ascending (TRUE) |
-| SORTBY | `[Table].SORTBY([Table].[SortCol], [ascending]).[OutputCol]` | List | Sort by column then extract output column. **Chain-only, must include output column** |
-| UNIQUE | `UNIQUE(list)` | List | Deduplicate |
-| ARRAYJOIN | `ARRAYJOIN(list, [delimiter])` | Text | Join list elements as text; default comma-separated |
-| LISTCOMBINE | `LISTCOMBINE(val1, [val2, ...])` or `list.LISTCOMBINE()` | List | Two uses: (1) merge values/lists into one list; (2) chained call to flatten 2D array (commonly used when FILTER result column is a multi-value field) |
-| DISTANCE | `DISTANCE(location1, location2)` | Number | Distance between two geographic locations (km) |
+| Function | Signature | Return type | Description |
+|---|---|---|---|
+| FIRST | `FIRST(list)` | Scalar | First element |
+| LAST | `LAST(list)` | Scalar | Last element |
+| FILTER | `[Table].FILTER(condition).[ResultCol]` or `[Table].[Col].FILTER(condition)` | List | Filter by condition. When data range is a table, result column is **required**; when it's a column/list, it's not needed. Use CurrentValue in conditions. Add `.LISTCOMBINE()` when result column is multi-value |
+| MAP | `data_range.MAP(mapping_expr)` | List | Apply mapping to each element. Use CurrentValue in mapping |
+| SORT | `SORT(list, [ascending])` | List | Sort; default ascending (TRUE) |
+| SORTBY | `[Table].SORTBY([Table].[SortCol], [ascending]).[OutputCol]` | List | Sort by column then extract output column. **Chain-only, must include output column** |
+| UNIQUE | `UNIQUE(list)` | List | Deduplicate |
+| ARRAYJOIN | `ARRAYJOIN(list, [delimiter])` | Text | Join list elements as text; default comma-separated |
+| LISTCOMBINE | `LISTCOMBINE(val1, [val2, ...])` or `list.LISTCOMBINE()` | List | Two uses: (1) merge values/lists into one list; (2) chained call to flatten 2D array (commonly used when FILTER result column is a multi-value field) |
---
-
## Section 9: Commonly Confused Functions
### CONTAIN vs CONTAINTEXT
-| | CONTAIN | CONTAINTEXT |
-| ----------- | -------------------------------------------------------------- | ---------------------------------------------------------- |
-| Purpose | Tests if a **list / `select` (`multiple=true`)** contains a value | Tests if **text** contains a substring |
-| Example | `[Tags].CONTAIN("Urgent")` | `[Notes].CONTAINTEXT("completed")` |
+| | CONTAIN | CONTAINTEXT |
+|---|---|---|
+| Purpose | Tests if a **list / `select` (`multiple=true`)** contains a value | Tests if **text** contains a substring |
+| Example | `[Tags].CONTAIN("Urgent")` | `[Notes].CONTAINTEXT("completed")` |
| Wrong usage | `CONTAIN([Notes], "completed")` — cannot do substring matching | `CONTAINTEXT([Tags], "Urgent")` — Tags is a list, not text |
### ISBLANK vs ISNULL
-| | ISBLANK | ISNULL |
-| ----------------- | ------- | ------ |
-| NULL | TRUE | TRUE |
-| `""` empty string | TRUE | FALSE |
-| Empty list `[]` | TRUE | FALSE |
-| `0` | FALSE | FALSE |
-| `FALSE` | FALSE | FALSE |
+| | ISBLANK | ISNULL |
+|---|---|---|
+| NULL | TRUE | TRUE |
+| `""` empty string | TRUE | FALSE |
+| Empty list `[]` | TRUE | FALSE |
+| `0` | FALSE | FALSE |
+| `FALSE` | FALSE | FALSE |
### DAYS vs DATEDIF
-| | DAYS | DATEDIF |
-| --------------- | ------------------------------------------------------------ | ----------------------------------------- |
-| Parameter order | `DAYS(end, start)` — end first | `DATEDIF(start, end, unit)` — start first |
-| Precision | Includes decimals (hours/minutes/seconds as fractional days) | Integer only (whole days/months/years) |
-| Negative values | Returns negative when start is after end | **Errors** when start is after end |
+| | DAYS | DATEDIF |
+|---|---|---|
+| Parameter order | `DAYS(end, start)` — end first | `DATEDIF(start, end, unit)` — start first |
+| Precision | Includes decimals (hours/minutes/seconds as fractional days) | Integer only (whole days/months/years) |
+| Negative values | Returns negative when start is after end | **Errors** when start is after end |
### SUM vs SUMIF
-| | SUM | SUMIF |
-| --------- | ---------------------------------------------- | -------------------------------------------------------------- |
-| Purpose | Sum all values | Sum values **matching a condition** |
-| Arguments | `SUM(val1, val2, ...)` or `SUM([Table].[Col])` | `SUMIF(data_range, condition)` with CurrentValue in condition |
-| Example | `SUM([Orders].[Amount])` — sum all | `SUMIF([Orders].[Amount], CurrentValue > 100)` — sum only >100 |
+| | SUM | SUMIF |
+|---|---|---|
+| Purpose | Sum all values | Sum values **matching a condition** |
+| Arguments | `SUM(val1, val2, ...)` or `SUM([Table].[Col])` | `SUMIF(data_range, condition)` with CurrentValue in condition |
+| Example | `SUM([Orders].[Amount])` — sum all | `SUMIF([Orders].[Amount], CurrentValue > 100)` — sum only >100 |
### FILTER+aggregation vs COUNTIF/SUMIF
-| | FILTER+aggregation | COUNTIF/SUMIF |
-| ----------- | ----------------------------------------------------- | ------------------------------------------------------------------------------ |
-| Nature | Filter then aggregate (two steps) | One-step (syntactic sugar) |
-| Equivalence | `[Table].FILTER(cond).[Col].LISTCOMBINE().SUM()` | `SUMIF([Table].[Col], cond)` (only when condition involves only column values) |
-| When to use | Conditions span multiple fields, or multi-step needed | Conditions only involve column values (e.g. `CurrentValue > 100`) |
+| | FILTER+aggregation | COUNTIF/SUMIF |
+|---|---|---|
+| Nature | Filter then aggregate (two steps) | One-step (syntactic sugar) |
+| Equivalence | `[Table].FILTER(cond).[Col].LISTCOMBINE().SUM()` | `SUMIF([Table].[Col], cond)` (only when condition involves only column values) |
+| When to use | Conditions span multiple fields, or multi-step needed | Conditions only involve column values (e.g. `CurrentValue > 100`) |
---
@@ -612,119 +576,11 @@ Reason: NOW, TODAY, PI and other zero-argument functions must include parenthese
---
-## Section 13: Complete Examples
+## Section 13: Examples
-### Example 1: Employee sales summary
+完整示例与"自然语言需求 → 公式"翻译规则按需读 [formula-examples.md](formula-examples.md)。
-**Table structure** (from `+table-get`):
-
-- Employees: EmployeeID (Text), Name (Text), Department (Text)
-- Sales: ContractID (Number), SalespersonID (Text), Quantity (Number), Total (Number)
-
-**Current table**: Employees
-
-**Requirement**: For each employee, output "Sold XX orders" if they have sales records, otherwise "No sales records".
-
-**Formula**:
-
-```
-IF(
- [Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) >= 1,
- "Sold " & [Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) & " orders",
- "No sales records"
-)
-```
-
-**Field JSON**:
-
-```json
-{
- "type": "formula",
- "name": "Sales Summary",
- "expression": "IF([Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) >= 1, \"Sold \" & [Sales].COUNTIF(CurrentValue.[SalespersonID] = [EmployeeID]) & \" orders\", \"No sales records\")"
-}
-```
-
-**Explanation**: `[Sales].COUNTIF(...)` uses the entire Sales table as data range. CurrentValue represents each row in Sales, accessing `CurrentValue.[SalespersonID]` for that row's salesperson. `[EmployeeID]` refers to the current row in the Employees table (where the formula lives).
-
-### Example 2: Chained cross-table access via link fields
-
-**Table structure**:
-
-- Orders: ID (`auto_number`), OrderItems (`link` [target: OrderItems, foreign key: ID])
-- OrderItems: ID (`auto_number`), Product (`link` [target: Products, foreign key: ID])
-- Products: ID (`auto_number`), ProductName (`text`)
-
-**Current table**: Orders
-
-**Requirement**: Deduplicate and comma-join all product names from linked order items.
-
-**Formula**:
-
-```
-[OrderItems].[Product].[ProductName].UNIQUE().ARRAYJOIN(",")
-```
-
-**Field JSON**:
-
-```json
-{
- "type": "formula",
- "name": "Product List",
- "expression": "[OrderItems].[Product].[ProductName].UNIQUE().ARRAYJOIN(\",\")"
-}
-```
-
-**Explanation**: `[OrderItems]` gets linked order item records, `.[Product]` expands to each item's linked product, `.[ProductName]` gets all product names, `.UNIQUE()` deduplicates, `.ARRAYJOIN(",")` joins with commas.
-
-### Example 3: Cross-table filter + sort
-
-**Table structure**:
-
-- Projects: ProjectName (Text), Status (Text), Owner (Text)
-- Tasks: TaskName (Text), Project (Text), Priority (Number), DueDate (Date)
-
-**Current table**: Projects
-
-**Requirement**: Find the highest-priority (lowest number) task name for the current project.
-
-**Formula**:
-
-```
-FIRST(
- [Tasks].FILTER(CurrentValue.[Project] = [ProjectName]).SORTBY([Tasks].[Priority], TRUE).[TaskName]
-)
-```
-
-**Field JSON**:
-
-```json
-{
- "type": "formula",
- "name": "Top Priority Task",
- "expression": "FIRST([Tasks].FILTER(CurrentValue.[Project] = [ProjectName]).SORTBY([Tasks].[Priority], TRUE).[TaskName])"
-}
-```
-
-**Explanation**: `[Tasks].FILTER(CurrentValue.[Project] = [ProjectName])` filters tasks belonging to the current project. `.SORTBY([Tasks].[Priority], TRUE)` sorts by priority ascending. `.[TaskName]` extracts task names. `FIRST(...)` gets the first one (highest priority).
-
----
-
-## Section 14: Translating User Requirements to Formulas
-
-When the user describes their formula need in natural language, follow these rules to convert it into a precise expression:
-
-1. **Numbers must use precise values**: "less than 80%" → field value less than `0.8`. "above 1000" → `>= 1000`.
-2. **Interval boundaries**: "above/below/within" = closed (inclusive); "less than/more than/outside" = open (exclusive).
-3. **Branching logic** must be organized as an ordered list with a fallback branch. Each branch has a condition and output.
- - Example: "return risk level for 1-3" → `IFS([Value] = 1, "low", [Value] = 2, "medium", [Value] = 3, "high")` with an `IFERROR` or trailing empty-string fallback.
-4. **Multi-level branches must be flattened** to a single level. Nested if-else chains → flat IFS.
-5. **Branch conditions must be mutually exclusive**. If the user's conditions overlap, rewrite to eliminate ambiguity.
-6. **Reorder branches by logical priority** if the user's order is illogical (e.g., check specific conditions before catch-all).
-
----
-
-## Section 15: Constraint Summary
+## Section 14: Constraint Summary
- Request body must include `"type": "formula"` — this field is required
- Only use functions and operators listed in this document
diff --git a/skills/lark-base/references/formula-functions-extended.md b/skills/lark-base/references/formula-functions-extended.md
new file mode 100644
index 00000000..fd30b275
--- /dev/null
+++ b/skills/lark-base/references/formula-functions-extended.md
@@ -0,0 +1,66 @@
+# Base Formula Functions — Extended (rare functions)
+
+> 本文件是 [formula-field-guide.md](formula-field-guide.md) Section 8 的长尾补充:三角/双曲/随机数/进制/统计扩展等罕见函数。
+> 表格列含义与主文档一致:Function | Signature | Return type | Description。
+
+## 8.1 Logic functions (extended)
+
+| Function | Signature | Return type | Description |
+|---|---|---|---|
+| ISERROR | `ISERROR(expr)` | Boolean | Tests if expression errors |
+| ISNUMBER | `ISNUMBER(value)` | Boolean | Tests if value is a number |
+| CONTAINSALL | `CONTAINSALL(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains all specified values |
+| CONTAINSONLY | `CONTAINSONLY(search_range, value, ...)` | Boolean | Tests if a list or `select` (`multiple=true`) contains only the specified values |
+| TRUE | `TRUE()` | Boolean | Returns TRUE |
+| FALSE | `FALSE()` | Boolean | Returns FALSE |
+| RANDOMBETWEEN | `RANDOMBETWEEN(min_int, max_int, [keep_updating])` | Number | Random integer in the specified range |
+| RANDOMITEM | `RANDOMITEM(list, [keep_updating])` | Matches element type | Randomly picks one element from a list |
+
+## 8.2 Numeric functions (extended)
+
+| Function | Signature | Return type | Description |
+|---|---|---|---|
+| MEDIAN | `MEDIAN(val1, val2, ...)` | Number | Median |
+| ROUNDUP | `ROUNDUP(number, digits)` | Number | Round away from zero. Same digits semantics as ROUND |
+| ROUNDDOWN | `ROUNDDOWN(number, digits)` | Number | Round toward zero. Same digits semantics as ROUND |
+| FLOOR | `FLOOR(number, [base])` | Number | Round down to nearest multiple of base (default 1) |
+| CEILING | `CEILING(number, [base])` | Number | Round up to nearest multiple of base (default 1) |
+| POWER | `POWER(base, exponent)` | Number | Exponentiation |
+| QUOTIENT | `QUOTIENT(dividend, divisor)` | Number | Integer division |
+| ISODD | `ISODD(number)` | Boolean | Tests if number is odd |
+| RANK | `RANK(value, search_range, [ascending])` | Number | Rank of value in range; default descending |
+| SEQUENCE | `SEQUENCE(start, end, [step])` | List | Generate number sequence |
+| PI | `PI()` | Number | Pi constant |
+| SIN/COS/TAN/ASIN/ACOS/ATAN/ATAN2/SINH/COSH/TANH/ASINH/ACOSH/ATANH | `func(radians_or_value)` | Number | Trigonometric and hyperbolic functions; arguments in radians |
+
+## 8.3 Text functions (extended)
+
+| Function | Signature | Return type | Description |
+|---|---|---|---|
+| FIND | `FIND(search_val, search_range, [start])` | Number | Find substring position (case-sensitive); returns -1 if not found |
+| TODATE | `TODATE(value)` | Date | Convert date string to date type |
+| CHAR | `CHAR(number)` | Text | ASCII code to character |
+| FORMAT | `FORMAT(template, [val1, val2, ...])` | Text | Template string formatting; use `{1}`, `{2}` as placeholders |
+| HYPERLINK | `HYPERLINK(url, [display_text])` | Hyperlink | Create a hyperlink |
+| ENCODEURL | `ENCODEURL(text)` | Text | URL encode |
+| REGEXMATCH | `REGEXMATCH(text, regex)` | Boolean | Regex match test |
+| REGEXEXTRACT | `REGEXEXTRACT(text, regex)` | List | Extract first match's capture groups |
+| REGEXEXTRACTALL | `REGEXEXTRACTALL(text, regex)` | 2D List | Extract all matches |
+| REGEXREPLACE | `REGEXREPLACE(text, regex, replacement)` | Text | Regex replace |
+
+## 8.4 Date functions (extended)
+
+| Function | Signature | Return type | Description |
+|---|---|---|---|
+| DURATION | `DURATION(days, [hours], [minutes], [seconds])` | Duration | Create a duration for date arithmetic |
+| EDATE | `EDATE(date, months)` | Date | Date N months later |
+| EOMONTH | `EOMONTH(date, [months])` | Date | End of month N months later; months default 0 |
+| WORKDAY | `WORKDAY(start_date, days, [holidays])` | Date | Date N workdays later (skips weekends and holidays) |
+
+## 8.5 List functions (extended)
+
+| Function | Signature | Return type | Description |
+|---|---|---|---|
+| LIST | `LIST(val1, val2, ...)` | List | Create a list |
+| NTH | `NTH(list, index)` | Scalar | Nth element (1-based) |
+| DISTANCE | `DISTANCE(location1, location2)` | Number | Distance between two geographic locations (km) |
diff --git a/skills/lark-base/references/lark-base-cell-value.md b/skills/lark-base/references/lark-base-cell-value.md
index bb9cd663..2cacd887 100644
--- a/skills/lark-base/references/lark-base-cell-value.md
+++ b/skills/lark-base/references/lark-base-cell-value.md
@@ -13,6 +13,9 @@
- 一次 payload 里同一字段只用一种 key(字段名或字段 ID),不要重复。
- 写入前先 `+field-list` 获取字段 `type/style/multiple`,再构造值。
- 需要清空字段时优先传 `null`(字段允许清空时)。
+- 只写存储字段:系统字段、`formula`、`lookup` 只读;附件字段不走 CellValue,用 `+record-upload-attachment` / `+record-download-attachment` / `+record-remove-attachment`。
+- 批量写入单批最多 200 条(超出报 `1254104`);同一张表串行写,遇 `1254291` 并发冲突短暂等待后重试。
+- select/multiselect 写入未知选项会触发平台新增该选项;不是要新增时,先用 `+field-list` 或 `+field-search-options` 确认可选值。
## 2. 各类型 CellValue
diff --git a/skills/lark-base/references/lark-base-dashboard-usecase.md b/skills/lark-base/references/lark-base-dashboard-usecase.md
new file mode 100644
index 00000000..d78822ba
--- /dev/null
+++ b/skills/lark-base/references/lark-base-dashboard-usecase.md
@@ -0,0 +1,238 @@
+# Dashboard(仪表盘/数据看板)模块指引
+
+> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
+
+Dashboard 是 Base 中的数据可视化看板,可以把表格数据变成**组件**(图表、指标卡等)进行展示。
+
+## 核心概念
+
+- **Dashboard(仪表盘)**:容器,包含多个组件
+- **Block(组件)**:仪表盘中的单个可视化元素(柱状图、折线图、饼图、指标卡等)
+- **data_config**:组件的数据源配置(表名、字段、分组等)
+
+## 能力速览
+
+| 你想做什么 | 用这些命令 | 关键文档 |
+|------|-----------|---------|
+| 创建/删除/改名称 | `+dashboard-create/delete/update` | 本页下方「仪表盘管理」 |
+| 在仪表盘里添加组件 | `+dashboard-block-create` | 先用 `+table-list` 拿表,再用 `+field-list-batch --table-id <表1> --table-id <表2>` 批量拿字段,再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 构造 `data_config` |
+| 修改组件 | `+dashboard-block-update` | 先读 block 现状,再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 决定替换哪些顶层 key |
+| 查看仪表盘有哪些组件 | `+dashboard-get` 或 `+dashboard-block-list` | 本页下方「查看仪表盘」 |
+| 读取图表计算结果 | `+dashboard-block-get-data` | 返回图表最终数据协议;需要 block 元数据先用 `+dashboard-block-get` |
+| 智能重排组件布局 | `+dashboard-arrange` | 只在用户明确要求重排时执行;无法指定精确位置 |
+
+## 典型场景工作流
+
+### 场景 1:从 0 到 1 创建仪表盘
+
+示例:搭建一个销售数据分析仪表盘
+
+```bash
+# 第 1 步:创建空白仪表盘
+lark-cli base +dashboard-create --base-token xxx --name "销售数据分析"
+# 记录返回的 dashboard_id
+
+# 第 2 步:获取数据源信息
+lark-cli base +table-list --base-token xxx # 先拿表名/table_id
+lark-cli base +field-list-batch --base-token xxx --table-id tbl_a --table-id tbl_b # 再一次批量拿相关表字段
+
+# 第 3 步:规划应该创建哪些组件(根据用户需求确定组件类型和数量)
+# 例如:总销售额(指标卡)、月度趋势(折线图)、品类占比(饼图)
+
+# 第 4 步:顺序创建每个组件(必须串行执行,不能并发)
+# 重要:创建组件前,先确定 dashboard_id、组件 name/type 和真实表字段
+# 再阅读 dashboard-block-data-config.md 了解 data_config 结构、组件类型和 filter 规则
+
+# 第 1 个组件
+lark-cli base +dashboard-block-create \
+ --base-token xxx \
+ --dashboard-id blk_xxx \
+ --name "总销售额" \
+ --type statistics \
+ --data-config '{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}]}'
+
+# 第 2 个组件(等上一个完成后再执行)
+lark-cli base +dashboard-block-create \
+ --base-token xxx \
+ --dashboard-id blk_xxx \
+ --name "月度趋势" \
+ --type line \
+ --data-config '{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}],"group_by":[{"field_name":"月份","mode":"integrated"}]}'
+
+# 继续创建其他组件...
+
+# 第 5 步:组件创建完成后,使用 arrange 命令智能重排布局(可选但推荐)
+# 默认布局可能不够美观,arrange 会根据组件数量和类型自动优化布局
+lark-cli base +dashboard-arrange \
+ --base-token xxx \
+ --dashboard-id blk_xxx
+```
+
+### 场景 2:在已有仪表盘上添加新组件
+
+```bash
+# 第 1 步:列出仪表盘,定位到当前仪表盘
+lark-cli base +dashboard-list --base-token xxx
+# 获取目标 dashboard_id
+
+# 第 2 步:根据用户诉求规划组件类型和数据源
+# 建议先查看当前仪表盘已有组件,避免重复创建,或作为参考
+lark-cli base +dashboard-get --base-token xxx --dashboard-id blk_xxx
+
+# 第 3 步:获取数据源信息
+lark-cli base +table-list --base-token xxx # 先拿表名/table_id
+lark-cli base +field-list-batch --base-token xxx --table-id tbl_a --table-id tbl_b # 再一次批量拿相关表字段
+
+# 第 4 步:顺序创建每个新组件(必须串行执行,不能并发)
+# 重要:先确定 dashboard_id、组件 name/type 和真实表字段
+# 再阅读 dashboard-block-data-config.md 了解 data_config 结构
+lark-cli base +dashboard-block-create \
+ --base-token xxx \
+ --dashboard-id blk_xxx \
+ --name "新组件名" \
+ --type column \
+ --data-config '{...}'
+```
+
+### 场景 3:编辑已有组件
+
+> [!IMPORTANT]
+> `+dashboard-block-update` **不能修改组件的 `type`**(图表类型),只能更新 `name` 和 `data_config`。
+> 如需更换组件类型,必须先删除再重新创建。
+
+```bash
+# 第 1 步:列出仪表盘,定位到当前仪表盘
+lark-cli base +dashboard-list --base-token xxx
+
+# 第 2 步:列出组件,获取到目标组件
+lark-cli base +dashboard-block-list --base-token xxx --dashboard-id blk_xxx
+# 获取目标 block_id
+# 提示:查看已有组件可作为参考,或检查是否重复创建相似组件
+
+# 第 3 步:获取组件当前详情
+lark-cli base +dashboard-block-get --base-token xxx --dashboard-id blk_xxx --block-id chtxxxxxxxx
+
+# 第 4 步:根据用户编辑诉求准备更新
+# 如果编辑诉求涉及数据源变更,需要先获取数据源信息
+lark-cli base +table-list --base-token xxx # 先拿表名/table_id
+lark-cli base +field-list-batch --base-token xxx --table-id tbl_a --table-id tbl_b # 再一次批量拿相关表字段
+
+# 第 5 步:执行更新
+# 重要:先读取当前 block 的 name/type/data_config
+# 再阅读 dashboard-block-data-config.md 了解 data_config 更新规则
+lark-cli base +dashboard-block-update \
+ --base-token xxx \
+ --dashboard-id blk_xxx \
+ --block-id chtxxxxxxxx \
+ --data-config '{...}'
+```
+
+### 场景 4:重排仪表盘布局
+
+当用户明确要求对已有仪表盘进行布局重排或美化时使用。
+
+> [!CAUTION]
+> - 排列结果是**服务端智能推荐**,不一定完全符合用户预期
+> - 无法指定具体位置(如"第一排放 A,第二排放 B"),排列逻辑是**自适应**的
+> - **不建议**在已有仪表盘上自动调用,除非用户明确要求
+
+```bash
+# 第 1 步:列出仪表盘,定位到目标仪表盘
+lark-cli base +dashboard-list --base-token xxx
+
+# 第 2 步:执行智能重排
+lark-cli base +dashboard-arrange \
+ --base-token xxx \
+ --dashboard-id blk_xxx
+```
+
+### 场景 5:读取仪表盘或组件现状
+
+**选择查询方式:**
+- 想看仪表盘整体结构(含主题、所有组件名称和类型)→ 用 **方式 A**
+- 只想快速查看有哪些组件 → 用 **方式 B**
+- 想看某个组件的详细 data_config 配置 → 用 **方式 C**
+- 想看某个图表/指标卡实际算出来的数据 → 用 **方式 D**
+
+```bash
+# 第 1 步:列出仪表盘,定位到当前仪表盘
+lark-cli base +dashboard-list --base-token xxx
+
+# 第 2 步:根据用户诉求查看详情
+
+# 方式 A:查看仪表盘整体情况(包含所有组件列表)
+lark-cli base +dashboard-get --base-token xxx --dashboard-id blk_xxx
+
+# 方式 B:列出所有组件
+lark-cli base +dashboard-block-list --base-token xxx --dashboard-id blk_xxx
+
+# 方式 C:查看某个组件的详细配置
+lark-cli base +dashboard-block-get --base-token xxx --dashboard-id blk_xxx --block-id chtxxxxxxxx
+
+# 方式 D:查看某个图表组件的计算结果(AI 友好的 chart protocol)
+lark-cli base +dashboard-block-get-data --base-token xxx --block-id chtxxxxxxxx
+
+# 最后:把获取到的现状信息整理好告诉用户
+```
+
+## 组件类型选择
+
+组件 `type` 决定展示形式:
+
+| 用户想看什么 | 选什么 type | 说明 |
+|-------------|------------|------|
+| 数据趋势(时间变化) | line | 折线图组件 |
+| 类别比较(谁高谁低) | column | 柱状图组件 |
+| 占比分布(各部分比例) | pie | 饼图组件 |
+| 单个关键指标 | statistics | 指标卡组件 |
+| 富文本说明/标题/注释 | text | 文本组件(支持 Markdown) |
+
+详细组件类型和 data_config 完整规则:[dashboard-block-data-config.md](dashboard-block-data-config.md)
+
+## 常见问题
+
+**Q: 创建组件的命令和 data_config 怎么写?**
+A:
+1. 先确定 `dashboard_id`、组件 `name`、组件 `type` 和真实表字段
+2. 再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解:
+ - 全部组件类型的可复制模板
+ - filter 筛选条件格式
+ - 字段类型与操作符对应表
+
+**Q: 为什么组件创建失败了?**
+A: 常见原因:
+- `table_name` 用了 table_id 而不是表名(必须用表名称,如「订单表」)
+- `series` 和 `count_all` 同时存在(必须二选一,互斥)
+- 字段名拼写错误(必须用 `+field-list` 获取的真实字段名,禁止猜测)
+- 组件创建并发执行(必须串行,等上一个完成再执行下一个)
+
+**Q: 可以一次创建多个组件吗?**
+A: 不可以,必须串行执行。等上一个 `+dashboard-block-create` 完成后再执行下一个。
+
+**Q: 组件的 `type` 创建后能改吗?**
+A: 不能。`+dashboard-block-update` 只能修改 `name` 和 `data_config`,不能修改 `type`。
+
+**Q: 更新组件的命令和 data_config 怎么写?**
+A:
+1. 先读取当前 block,确认 `block_id`、当前 `type` 和已有 `data_config`
+2. 再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 了解 data_config 结构
+
+**data_config 更新策略(顶层 key merge)**:
+- 只传入需要修改的顶层字段(如 `series`、`filter`)
+- 未传的顶层字段(如 `group_by`)自动保留原值
+- 但每个传入的字段内部是**全量替换**(如传新 `filter` 会完整覆盖旧 `filter`)
+
+**Q: 查看已有组件有什么用?**
+A: 在「添加新组件」或「编辑组件」前查看已有组件可以:
+- 了解当前仪表盘已有哪些可视化
+- 避免重复创建相似的组件
+- 参考已有组件的 data_config 结构作为模板
+
+**Q: 我想直接拿图表算好的结果给 AI 分析,应该用什么?**
+A: 用 `+dashboard-block-get-data`。它返回图表协议 JSON(常见字段包括 `dimensions`、`measures`、`main_data`,指标卡可能还有 `comparison_data`、`trend_data`),不返回 block 名称、类型、布局或 `data_config`;需要这些元数据时先用 `+dashboard-block-get`。
+
+## 写入前检查
+
+- 创建 block 前必须知道 `base_token`、`dashboard_id`、组件 `name/type` 和 `data_config`。
+- 更新 block 前必须知道 `base_token`、`dashboard_id`、`block_id`,并读过当前 block。
+- `data_config` 中使用表名和字段名,不使用 table_id / field_id;名称必须来自 `+table-list` / `+field-list` 的真实返回。
diff --git a/skills/lark-base/references/lark-base-dashboard.md b/skills/lark-base/references/lark-base-dashboard.md
index 87aaaff9..4faa22aa 100644
--- a/skills/lark-base/references/lark-base-dashboard.md
+++ b/skills/lark-base/references/lark-base-dashboard.md
@@ -21,6 +21,14 @@ Dashboard 是 Base 中的数据可视化看板,可以把表格数据变成**
| 读取图表计算结果 | `+dashboard-block-get-data` | 返回图表最终数据协议;需要 block 元数据先用 `+dashboard-block-get` |
| 智能重排组件布局 | `+dashboard-arrange` | 只在用户明确要求重排时执行;无法指定精确位置 |
+## 执行要点
+
+- 创建/改图前先用 `+table-list` 拿表,再用 `+field-list-batch --table-id <表1> --table-id <表2>` 一次取多表字段,不要逐表多次 `+field-list`,多余调用会显著抬高 token。
+- 布局/重排/撑满/排列美观直接用 `+dashboard-arrange`,不要尝试用 `+dashboard-block-update` 修改 layout,layout 不是 `data_config`。
+- block 换图表类型或换数据源表(`table_name`)时,删除旧 block 后用 `+dashboard-block-create` 新建;`+dashboard-block-update` 只适合同一数据源内改 `series/filter/group_by/name`。
+- 删除具名图表:`+dashboard-list` → `+dashboard-block-list` 精确匹配名称 → `+dashboard-block-delete`;长 `block_id` 用变量传参,避免手抄截断。
+- 完整 dashboard 用例(从需求到逐组件落地)按需读 [lark-base-dashboard-usecase.md](lark-base-dashboard-usecase.md)。
+
## 典型场景工作流
### 场景 1:从 0 到 1 创建仪表盘
diff --git a/skills/lark-base/references/lark-base-field-json.md b/skills/lark-base/references/lark-base-field-json.md
index bfe77c3a..8be5e84d 100644
--- a/skills/lark-base/references/lark-base-field-json.md
+++ b/skills/lark-base/references/lark-base-field-json.md
@@ -397,6 +397,7 @@
默认值 / 约束:
- `style.rules` 是规则数组,数量 `1..9`
+- `+field-update` 修改编号规则时,**默认会把新规则应用到已有记录**
- 默认规则:
```json
diff --git a/skills/lark-base/references/lark-base-workflow-guide.md b/skills/lark-base/references/lark-base-workflow-guide.md
index fb7aa363..1cf0f095 100644
--- a/skills/lark-base/references/lark-base-workflow-guide.md
+++ b/skills/lark-base/references/lark-base-workflow-guide.md
@@ -1,46 +1,130 @@
# Workflow guide
-本文档是 Workflow 的入口指南,帮助选择步骤组合、理解创建/更新边界,并引导到 steps JSON SSOT。
+本文档是 Workflow 的操作地图:先用它决定最短路径,再按需打开 schema 小文件。Guide 要一次读完后能完成大多数查询、启停和常见创建/修改;schema 才是零件手册。
-> **配套文档**:
-> - Workflow 的数据结构参考:[lark-base-workflow-schema.md](lark-base-workflow-schema.md)
-> - 创建/更新时重点构造 `title`、`status` 和 `steps`;复杂度集中在 `steps[].type/data/next`
+## 先判断任务类型
----
+| 目标 | 最短路径 | 是否读 schema |
+|---|---|---|
+| 列出 workflow | `+workflow-list --base-token `;需要筛选启停状态时用 `--status` | 不读 |
+| 查看一个 workflow | 先 `+workflow-list` 后按标题本地匹配 `workflow_id`,再 `+workflow-get --workflow-id ` | 不读,除非要解释完整 `steps` |
+| 启用/停用 workflow | `+workflow-list --status ` 定位,再 `+workflow-enable/disable` | 不读 |
+| 创建简单 workflow | 读本 guide,按下方场景表打开必要 step schema | 只读命中的 step |
+| 修改 workflow | `+workflow-get` 取现状,保留无关字段,只改目标 step;复杂 step 再读 schema | 只读被改的 step |
+| 解释复杂 `steps` | 先用本 guide 的结构速记理解连线,再按 step type 打开 schema | 按需读 |
-## 快速开始
+不要默认看 `--help`。只有命令报错、参数名不确定、或要确认复杂写入参数时,才看当前命令的 help。
-### 最简单的 Workflow
+## 资源发现顺序
-新增记录时发送消息通知:
+1. 从用户链接提取 `base_token`。
+2. 需要知道文档内资源时用 `+base-block-list` 或 `+table-list`;不要两者都跑,除非一个结果不够。
+3. 字段发现默认用 `+field-list --compact`;只有需要公式、lookup 或完整字段配置时再 `+field-get`。
+4. 多表字段发现用 `+field-list-batch --compact --table-id --table-id `。
+5. workflow 定位用 `+workflow-list` 读取列表,再按 `title` 本地匹配;当前命令没有 `--title` flag。
+
+## Workflow 结构速记
```json
{
- "client_token": "1704067200",
- "title": "新订单自动通知",
+ "client_token": "unique-create-token",
+ "title": "工作流标题",
"steps": [
{
- "id": "trigger_1",
+ "id": "step_trigger",
"type": "AddRecordTrigger",
- "title": "监控新订单",
- "next": "action_1",
+ "title": "触发器",
+ "next": "step_action",
+ "data": {}
+ },
+ {
+ "id": "step_action",
+ "type": "LarkMessageAction",
+ "title": "动作",
+ "next": null,
+ "data": {}
+ }
+ ]
+}
+```
+
+- `id` 要稳定、可读,被 `next` 和 `children.links[].to` 引用。
+- 普通 trigger/action 用 `next` 串联;最后一个节点 `next:null`。
+- `IfElseBranch` / `SwitchBranch` / `Loop` 用 `children.links` 表达分支或循环入口。
+- Action 节点不要设置 `children`。
+- `ref` 引用前置 step 的输出,字段下钻通常是 `$.{stepId}.{fieldId}`;循环内当前项常用 `$.{loopStepId}.item.{fieldId}`。
+- `+workflow-create` 需要唯一 `client_token`;新 workflow 创建后默认 disabled,用户需要启用时再调用 `+workflow-enable`。
+- `+workflow-update` 是完整替换;从 `+workflow-get` 返回中保留不想改的 `title/status/steps`。
+
+## Step 选型
+
+创建/修改前先产出一个草图:列出全部节点 `id/type/next/children`,把会用到的 `type` 去重后,再一次性读取对应的 step md 文档。不要“读一个 step、想一轮、再读下一个 step”;这会增加轮次和上下文重放。
+
+| 用户说法 | 选型 |
+|---|---|
+| 新增记录时 | `AddRecordTrigger` |
+| 记录被修改时 | `SetRecordTrigger` |
+| 新增或修改都触发、或拿不准 | `ChangeRecordTrigger` |
+| 每天/每周/每月/固定时间 | `TimerTrigger` |
+| 日期字段到期提醒 | `ReminderTrigger` |
+| 点击按钮 | `ButtonTrigger` |
+| 收到群消息/私聊消息 | `LarkMessageTrigger` |
+| 新增一条记录 | `AddRecordAction` |
+| 更新当前或查找到的记录 | `SetRecordAction` |
+| 查找多条记录再处理 | `FindRecordAction`,多条时接 `Loop` |
+| 分两路判断 | `IfElseBranch` |
+| 多档位/多类别判断 | `SwitchBranch` |
+| 发送飞书消息 | `LarkMessageAction` |
+| 调外部接口 | `HTTPClientAction` |
+| 等待一段时间 | `Delay` |
+| AI 生成文本 | `GenerateAiTextAction` |
+
+用户描述"修改为 X **或** 新增 X 时"这类同条件多来源需求,是单个 `ChangeRecordTrigger` + `condition_list` 的典型场景,一条工作流即可表达,不要拆成 `AddRecordTrigger` 和 `SetRecordTrigger` 两条工作流。
+
+## 常见场景
+
+| 场景 | 推荐步骤 | 需要读的 schema |
+|---|---|---|
+| 新增记录后发通知 | `AddRecordTrigger -> LarkMessageAction` | `trigger-add-record.md`, `action-lark-message.md` |
+| 记录变化后更新同一行字段 | `ChangeRecordTrigger -> SetRecordAction` | `trigger-change-record.md`, `action-set-record.md`; 条件复杂再读 common refs |
+| 金额/状态分档处理 | `AddRecordTrigger -> SwitchBranch -> SetRecordAction...` | `trigger-add-record.md`, `branch-switch.md`, `action-set-record.md`, common conditions |
+| 二选一判断 | `... -> IfElseBranch -> ...` | `branch-if-else.md`, common conditions |
+| 定时汇总并逐人通知 | `TimerTrigger -> FindRecordAction -> Loop -> LarkMessageAction` | `trigger-timer.md`, `action-find-record.md`, `system-loop.md`, `action-lark-message.md`, common refs |
+| 群消息触发后回复 | `LarkMessageTrigger -> FindRecordAction/Loop -> LarkMessageAction` | `trigger-lark-message.md`, `action-find-record.md`, `system-loop.md`, `action-lark-message.md` |
+| 按钮触发外部系统 | `ButtonTrigger -> HTTPClientAction -> AddRecordAction` | `trigger-button.md`, `action-http-client.md`, `action-add-record.md` |
+| 调用 AI 生成内容并写回 | `... -> GenerateAiTextAction -> SetRecordAction` | `action-generate-ai-text.md`, `action-set-record.md`, common refs |
+
+Schema 入口:[lark-base-workflow-schema.md](lark-base-workflow-schema.md)。不要一次性打开所有 step 文件;先确定本次 workflow 的完整 step type 集合,再一次性打开这些文件。只有确定会写 `ref`、条件、字段值或节点输出引用时,才把 `common-types-and-refs.md` 加入同一批读取。
+
+## 最小例子:新增记录后发送消息
+
+只读 `trigger-add-record.md` 和 `action-lark-message.md` 即可。
+
+```json
+{
+ "client_token": "wf-unique-token",
+ "title": "新订单通知",
+ "steps": [
+ {
+ "id": "trig_new_order",
+ "type": "AddRecordTrigger",
+ "title": "新增订单时",
+ "next": "act_notify",
"data": {
"table_name": "订单表",
"watched_field_name": "订单号"
}
},
{
- "id": "action_1",
+ "id": "act_notify",
"type": "LarkMessageAction",
"title": "发送通知",
"next": null,
"data": {
- "receiver": [{ "value_type": "user", "value": {"id": "ou_xxxx", "name": "张三"} }],
+ "receiver": [{ "value_type": "user", "value": { "id": "ou_xxx" } }],
"send_to_everyone": false,
"title": [{ "value_type": "text", "value": "新订单提醒" }],
- "content": [
- { "value_type": "text", "value": "收到新订单" }
- ],
+ "content": [{ "value_type": "text", "value": "收到新订单" }],
"btn_list": []
}
}
@@ -48,783 +132,27 @@
}
```
----
+## 修改现有 workflow
-## 场景速查表
+1. `+workflow-list` 后按标题定位 `workflow_id`。
+2. `+workflow-get --workflow-id ` 获取完整定义。
+3. 只修改目标 step,保留其他 steps 的 `id/type/title/data/next/children`。
+4. 用 `+workflow-update` 提交完整定义。
+5. 若只启停,不走 update,直接 `+workflow-enable/disable`。
-| 场景 | 步骤组合 | 示例 |
-|------|---------|------|
-| 新增触发+通知 | AddRecordTrigger → LarkMessageAction | [下方](#示例1-新增记录触发--发送消息) |
-| 按钮点击+调用外部接口+写入日志 | ButtonTrigger → HTTPClientAction → AddRecordAction | [下方](#示例-6-按钮触发--调用外部接口--写入同步日志) |
-| 定时+循环 | TimerTrigger → FindRecordAction → Loop → LarkMessageAction | [下方](#示例2-定时触发--查找记录--循环遍历--发送消息) |
-| 条件判断 | ... → IfElseBranch → 分支处理 | [下方](#示例3-条件分支-ifelsebranch) |
-| 多路分类 | ... → SwitchBranch → 多分支处理 | [下方](#示例4-多路分支-switchbranch) |
-| 复杂组合 | 定时+查找+循环+分支+消息 | [下方](#示例5-组合场景-定时查找循环分支消息) |
+## 常见错误
----
-
-## 完整示例
-
-### 示例 1: 新增记录触发 + 发送消息
-
-**场景**: 当订单表新增记录时,发送飞书消息通知负责人。
-
-```json
-{
- "client_token": "1704067201",
- "title": "新订单自动通知",
- "steps": [
- {
- "id": "step_trigger",
- "type": "AddRecordTrigger",
- "title": "新增订单时触发",
- "next": "step_notify",
- "data": {
- "table_name": "订单表",
- "watched_field_name": "订单号",
- "condition_list": null
- }
- },
- {
- "id": "step_notify",
- "type": "LarkMessageAction",
- "title": "发送订单通知",
- "next": null,
- "data": {
- "receiver": [{ "value_type": "ref", "value": "$.step_trigger.fldManager" }],
- "send_to_everyone": false,
- "title": [{ "value_type": "text", "value": "新订单提醒" }],
- "content": [
- { "value_type": "text", "value": "客户 " },
- { "value_type": "ref", "value": "$.step_trigger.fldCustomer" },
- { "value_type": "text", "value": " 创建了新订单,金额:¥" },
- { "value_type": "ref", "value": "$.step_trigger.fldAmount" }
- ],
- "btn_list": [
- {
- "text": "查看订单",
- "btn_action": "openLink",
- "link": [{ "value_type": "ref", "value": "$.step_trigger.recordLink" }]
- }
- ]
- }
- }
- ]
-}
-```
-
-**关键点**:
-- `AddRecordTrigger` 监控 `table_name` 表的 `watched_field_name` 字段
-- 使用 `ref` 引用触发器输出的字段值(注意是 fieldId,不是字段名)
-- `recordLink` 是触发器内置输出,表示记录链接
-
----
-
-### 示例 2: 定时触发 + 查找记录 + 循环遍历 + 发送消息
-
-**场景**: 每天早上 9 点,查找所有待处理订单,给每个客户发送提醒。
-
-```json
-{
- "client_token": "1704067202",
- "title": "每日待处理订单提醒",
- "steps": [
- {
- "id": "step_timer",
- "type": "TimerTrigger",
- "title": "每天早上9点触发",
- "next": "step_find_orders",
- "data": {
- "rule": "DAILY",
- "start_time": "2025-01-01 09:00",
- "is_never_end": true
- }
- },
- {
- "id": "step_find_orders",
- "type": "FindRecordAction",
- "title": "查找所有待处理订单",
- "next": "step_loop_customers",
- "data": {
- "table_name": "订单表",
- "field_names": ["客户名称", "订单金额", "客户联系方式"],
- "should_proceed_when_no_results": false,
- "filter_info": {
- "conjunction": "and",
- "conditions": [
- {
- "field_name": "状态",
- "operator": "is",
- "value": [{ "value_type": "option", "value": { "name": "待处理" } }]
- }
- ]
- }
- }
- },
- {
- "id": "step_loop_customers",
- "type": "Loop",
- "title": "遍历每个订单",
- "children": {
- "links": [
- { "kind": "loop_start", "to": "step_send_reminder" }
- ]
- },
- "next": null,
- "data": {
- "loop_mode": "continue",
- "max_loop_times": 100,
- "data": [{
- "value_type": "ref",
- "value": "$.step_find_orders.fieldRecords"
- }]
- }
- },
- {
- "id": "step_send_reminder",
- "type": "LarkMessageAction",
- "title": "发送催办消息",
- "next": null,
- "data": {
- "receiver": [{
- "value_type": "ref",
- "value": "$.step_loop_customers.item.fldContact"
- }],
- "send_to_everyone": false,
- "title": [{ "value_type": "text", "value": "订单处理提醒" }],
- "content": [
- { "value_type": "text", "value": "您好,您的订单 " },
- { "value_type": "ref", "value": "$.step_loop_customers.item.fldName" },
- { "value_type": "text", "value": " 金额 ¥" },
- { "value_type": "ref", "value": "$.step_loop_customers.item.fldAmount" },
- { "value_type": "text", "value": " 正在处理中。" }
- ],
- "btn_list": []
- }
- }
- ]
-}
-```
-
-**关键点**:
-- `Loop.data` 必须传入 `ref` 类型的数据源(通常是 FindRecordAction 的 `fieldRecords`)
-- `Loop.children.links` 必须包含 `kind: "loop_start"` 的链接指向循环体
-- 循环体内用 `$.{loopStepId}.item.{fieldId}` 引用当前遍历记录的字段
-- `$.{loopStepId}.index` 获取当前索引(从 0 开始)
-
----
-
-### 示例 3: 条件分支(IfElseBranch)
-
-**场景**: 根据订单金额判断,大额订单通知主管审批,小额订单自动通过。
-
-```json
-{
- "client_token": "1704067203",
- "title": "订单金额自动判断",
- "steps": [
- {
- "id": "step_trigger",
- "type": "AddRecordTrigger",
- "title": "新增订单时触发",
- "next": "step_check_amount",
- "data": {
- "table_name": "订单表",
- "watched_field_name": "订单金额"
- }
- },
- {
- "id": "step_check_amount",
- "type": "IfElseBranch",
- "title": "判断是否为大额订单",
- "children": {
- "links": [
- { "kind": "if_true", "to": "step_notify_manager", "label": "high", "desc": "金额>=10000" },
- { "kind": "if_false", "to": "step_auto_approve", "label": "normal", "desc": "金额<10000" }
- ]
- },
- "next": "step_log",
- "data": {
- "condition": {
- "conjunction": "or",
- "conditions": [
- {
- "conjunction": "and",
- "conditions": [
- {
- "left_value": { "value_type": "ref", "value": "$.step_trigger.fldAmount" },
- "operator": "isGreaterEqual",
- "right_value": [{ "value_type": "number", "value": 10000 }]
- }
- ]
- }
- ]
- }
- }
- },
- {
- "id": "step_notify_manager",
- "type": "LarkMessageAction",
- "title": "通知主管审批大额订单",
- "next": "step_log",
- "data": {
- "receiver": [{ "value_type": "user", "value": {"id": "ou_manager", "name": "主管"} }],
- "send_to_everyone": false,
- "title": [{ "value_type": "text", "value": "大额订单待审批" }],
- "content": [
- { "value_type": "text", "value": "有大额订单 ¥" },
- { "value_type": "ref", "value": "$.step_trigger.fldAmount" },
- { "value_type": "text", "value": " 需要您审批" }
- ],
- "btn_list": []
- }
- },
- {
- "id": "step_auto_approve",
- "type": "SetRecordAction",
- "title": "自动标记小额订单为已审核",
- "next": "step_log",
- "data": {
- "table_name": "订单表",
- "ref_info": { "step_id": "step_trigger" },
- "field_values": [
- {
- "field_name": "审批状态",
- "value": [{ "value_type": "option", "value": { "name": "已自动审核" } }]
- }
- ]
- }
- },
- {
- "id": "step_log",
- "type": "GenerateAiTextAction",
- "title": "生成订单处理日志",
- "next": null,
- "data": {
- "prompt": [
- { "value_type": "text", "value": "请生成订单处理日志,金额:" },
- { "value_type": "ref", "value": "$.step_trigger.fldAmount" }
- ]
- }
- }
- ]
-}
-```
-
-**关键点**:
-- `IfElseBranch.children.links` 必须包含 `if_true` 和 `if_false` 两个分支
-- `next` 指向两个分支汇合后的步骤(可选,为 null 则分支结束)
-- `condition` 使用 OrGroup 结构,支持 `(A and B) or (C and D)` 的复杂条件
-- 分支内可以用 `ref_info` 引用触发记录,用 `filter_info` 批量筛选记录
-
----
-
-### 示例 4: 多路分支(SwitchBranch)
-
-**场景**: 根据订单优先级(P0/P1/P2)执行不同的处理流程。
-
-```json
-{
- "client_token": "1704067204",
- "title": "按优先级分类处理订单",
- "steps": [
- {
- "id": "step_trigger",
- "type": "AddRecordTrigger",
- "title": "新增订单时触发",
- "next": "step_classify",
- "data": {
- "table_name": "订单表",
- "watched_field_name": "优先级"
- }
- },
- {
- "id": "step_classify",
- "type": "SwitchBranch",
- "title": "按优先级分类",
- "children": {
- "links": [
- { "kind": "case", "to": "step_p0_handler", "label": "p0", "desc": "P0-紧急" },
- { "kind": "case", "to": "step_p1_handler", "label": "p1", "desc": "P1-高优先级" },
- { "kind": "case", "to": "step_p2_handler", "label": "p2", "desc": "P2-普通" },
- { "kind": "case", "to": "step_other_handler", "label": "other", "desc": "其他" }
- ]
- },
- "next": null,
- "data": {
- "mode": "exclusive",
- "no_match_action": "classifyToOther",
- "child_branch_list": [
- {
- "name": "P0-紧急",
- "condition": {
- "conjunction": "or",
- "conditions": [
- {
- "conjunction": "and",
- "conditions": [
- {
- "left_value": { "value_type": "ref", "value": "$.step_trigger.fldPriority" },
- "operator": "is",
- "right_value": [{ "value_type": "option", "value": { "name": "P0" } }]
- }
- ]
- }
- ]
- }
- },
- {
- "name": "P1-高优先级",
- "condition": {
- "conjunction": "or",
- "conditions": [
- {
- "conjunction": "and",
- "conditions": [
- {
- "left_value": { "value_type": "ref", "value": "$.step_trigger.fldPriority" },
- "operator": "is",
- "right_value": [{ "value_type": "option", "value": { "name": "P1" } }]
- }
- ]
- }
- ]
- }
- },
- {
- "name": "P2-普通",
- "condition": {
- "conjunction": "or",
- "conditions": [
- {
- "conjunction": "and",
- "conditions": [
- {
- "left_value": { "value_type": "ref", "value": "$.step_trigger.fldPriority" },
- "operator": "is",
- "right_value": [{ "value_type": "option", "value": { "name": "P2" } }]
- }
- ]
- }
- ]
- }
- }
- ]
- }
- },
- {
- "id": "step_p0_handler",
- "type": "LarkMessageAction",
- "title": "P0紧急处理",
- "next": null,
- "data": {
- "receiver": [{ "value_type": "user", "value": {"id": "ou_director", "name": "总监"} }],
- "send_to_everyone": false,
- "title": [{ "value_type": "text", "value": "🚨 P0 紧急订单" }],
- "content": [{ "value_type": "text", "value": "有新的 P0 紧急订单需要立即处理" }],
- "btn_list": []
- }
- },
- {
- "id": "step_p1_handler",
- "type": "SetRecordAction",
- "title": "标记高优先级",
- "next": null,
- "data": {
- "table_name": "订单表",
- "ref_info": { "step_id": "step_trigger" },
- "field_values": [
- { "field_name": "处理状态", "value": [{ "value_type": "text", "value": "高优先级待处理" }] }
- ]
- }
- },
- {
- "id": "step_p2_handler",
- "type": "Delay",
- "title": "普通订单延迟处理",
- "next": null,
- "data": { "duration": 60 }
- },
- {
- "id": "step_other_handler",
- "type": "SetRecordAction",
- "title": "标记其他订单",
- "next": null,
- "data": {
- "table_name": "订单表",
- "ref_info": { "step_id": "step_trigger" },
- "field_values": [
- { "field_name": "处理状态", "value": [{ "value_type": "text", "value": "待分类" }] }
- ]
- }
- }
- ]
-}
-```
-
-**关键点**:
-- `SwitchBranch` 适合 3 路及以上的分支场景(少于 3 路用 `IfElseBranch` 更简洁)
-- `children.links` 中 `kind: "case"` 的 `label` 对应 `child_branch_list` 中的条件
-- `mode: "exclusive"` 表示排他执行(第一个匹配的分支执行后停止)
-- `no_match_action: "classifyToOther"` 表示无匹配时走最后一个 `case`(兜底分支)
-
----
-
-### 示例 5: 组合场景(定时+查找+循环+分支+消息)
-
-**场景**: 每天早上 9 点,查找昨天的订单,按金额分级,给不同级别的销售发送不同的通知。
-
-```json
-{
- "client_token": "1704067205",
- "title": "每日订单分级通知",
- "steps": [
- {
- "id": "step_timer",
- "type": "TimerTrigger",
- "title": "每天早上9点触发",
- "next": "step_find_orders",
- "data": {
- "rule": "DAILY",
- "start_time": "2025-01-01 09:00",
- "is_never_end": true
- }
- },
- {
- "id": "step_find_orders",
- "type": "FindRecordAction",
- "title": "查找昨天所有订单",
- "next": "step_loop",
- "data": {
- "table_name": "订单表",
- "field_names": ["订单号", "客户名称", "金额", "销售负责人"],
- "should_proceed_when_no_results": false,
- "filter_info": {
- "conjunction": "and",
- "conditions": [
- { "field_name": "创建时间", "operator": "isGreaterEqual", "value": [{ "value_type": "date", "value": "yesterday" }] }
- ]
- }
- }
- },
- {
- "id": "step_loop",
- "type": "Loop",
- "title": "遍历每个订单",
- "children": {
- "links": [
- { "kind": "loop_start", "to": "step_classify" }
- ]
- },
- "next": "step_summary",
- "data": {
- "loop_mode": "continue",
- "max_loop_times": 500,
- "data": [{ "value_type": "ref", "value": "$.step_find_orders.fieldRecords" }]
- }
- },
- {
- "id": "step_classify",
- "type": "SwitchBranch",
- "title": "按金额分类",
- "children": {
- "links": [
- { "kind": "case", "to": "step_vip_notify", "label": "vip", "desc": "VIP >= 10万" },
- { "kind": "case", "to": "step_normal_notify", "label": "normal", "desc": "普通 < 10万" }
- ]
- },
- "next": null,
- "data": {
- "mode": "exclusive",
- "no_match_action": "fail",
- "child_branch_list": [
- {
- "name": "VIP订单",
- "condition": {
- "conjunction": "or",
- "conditions": [
- {
- "conjunction": "and",
- "conditions": [
- {
- "left_value": { "value_type": "ref", "value": "$.step_loop.item.fldAmount" },
- "operator": "isGreaterEqual",
- "right_value": [{ "value_type": "number", "value": 100000 }]
- }
- ]
- }
- ]
- }
- },
- {
- "name": "普通订单",
- "condition": {
- "conjunction": "or",
- "conditions": [
- {
- "conjunction": "and",
- "conditions": [
- {
- "left_value": { "value_type": "ref", "value": "$.step_loop.item.fldAmount" },
- "operator": "isLess",
- "right_value": [{ "value_type": "number", "value": 100000 }]
- }
- ]
- }
- ]
- }
- }
- ]
- }
- },
- {
- "id": "step_vip_notify",
- "type": "LarkMessageAction",
- "title": "VIP订单通知",
- "next": null,
- "data": {
- "receiver": [{ "value_type": "ref", "value": "$.step_loop.item.fldSales" }],
- "send_to_everyone": false,
- "title": [{ "value_type": "text", "value": "🌟 VIP大额订单" }],
- "content": [
- { "value_type": "text", "value": "恭喜!您有一笔 VIP 订单 ¥" },
- { "value_type": "ref", "value": "$.step_loop.item.fldAmount" },
- { "value_type": "text", "value": ",客户:" },
- { "value_type": "ref", "value": "$.step_loop.item.fldCustomer" }
- ],
- "btn_list": []
- }
- },
- {
- "id": "step_normal_notify",
- "type": "LarkMessageAction",
- "title": "普通订单通知",
- "next": null,
- "data": {
- "receiver": [{ "value_type": "ref", "value": "$.step_loop.item.fldSales" }],
- "send_to_everyone": false,
- "title": [{ "value_type": "text", "value": "新订单通知" }],
- "content": [
- { "value_type": "text", "value": "您有一笔新订单 ¥" },
- { "value_type": "ref", "value": "$.step_loop.item.fldAmount" }
- ],
- "btn_list": []
- }
- },
- {
- "id": "step_summary",
- "type": "GenerateAiTextAction",
- "title": "生成日报",
- "next": null,
- "data": {
- "prompt": [
- { "value_type": "text", "value": "请生成昨日订单处理日报" }
- ]
- }
- }
- ]
-}
-```
-
----
-
-### 示例 6: 按钮触发 + 调用外部接口 + 写入同步日志
-
-**场景**: 在「客户线索表」里给每条记录配置一个“同步到 CRM”按钮。销售点击按钮后,Workflow 调用外部 CRM 接口同步当前线索,再在「同步日志表」新增一条记录,方便后续审计和排查。
-
-```json
-{
- "client_token": "1704067206",
- "title": "线索一键同步到 CRM",
- "steps": [
- {
- "id": "step_button_trigger",
- "type": "ButtonTrigger",
- "title": "点击同步到 CRM 按钮时触发",
- "next": "step_call_crm_api",
- "data": {
- "button_type": "buttonField",
- "table_name": "客户线索表"
- }
- },
- {
- "id": "step_call_crm_api",
- "type": "HTTPClientAction",
- "title": "调用 CRM 同步接口",
- "next": "step_add_sync_log",
- "data": {
- "method": "POST",
- "url": [
- { "value_type": "text", "value": "https://api.example-crm.com/v1/leads/sync" }
- ],
- "headers": [
- { "key": "Content-Type", "value": [{ "value_type": "text", "value": "application/json" }] },
- { "key": "X-System", "value": [{ "value_type": "text", "value": "lark_base_workflow" }] }
- ],
- "body_type": "raw",
- "raw_body": [
- { "value_type": "text", "value": "{\"lead_name\":\"" },
- { "value_type": "ref", "value": "$.step_button_trigger.fldLeadName" },
- { "value_type": "text", "value": "\",\"mobile\":\"" },
- { "value_type": "ref", "value": "$.step_button_trigger.fldMobile" },
- { "value_type": "text", "value": "\",\"company\":\"" },
- { "value_type": "ref", "value": "$.step_button_trigger.fldCompany" },
- { "value_type": "text", "value": "\",\"owner\":\"" },
- { "value_type": "ref", "value": "$.step_button_trigger.fldOwner" },
- { "value_type": "text", "value": "\",\"source_record_id\":\"" },
- { "value_type": "ref", "value": "$.step_button_trigger.recordId" },
- { "value_type": "text", "value": "\"}" }
- ],
- "response_type": "json",
- "response_value": "{\"success\":true,\"message\":\"lead synced successfully\"}"
- }
- },
- {
- "id": "step_add_sync_log",
- "type": "AddRecordAction",
- "title": "写入同步日志",
- "next": null,
- "data": {
- "table_name": "同步日志表",
- "field_values": [
- {
- "field_name": "线索名称",
- "value": [{ "value_type": "ref", "value": "$.step_button_trigger.fldLeadName" }]
- },
- {
- "field_name": "手机号",
- "value": [{ "value_type": "ref", "value": "$.step_button_trigger.fldMobile" }]
- },
- {
- "field_name": "公司名称",
- "value": [{ "value_type": "ref", "value": "$.step_button_trigger.fldCompany" }]
- },
- {
- "field_name": "负责人",
- "value": [{ "value_type": "ref", "value": "$.step_button_trigger.fldOwner" }]
- },
- {
- "field_name": "来源记录ID",
- "value": [{ "value_type": "ref", "value": "$.step_button_trigger.recordId" }]
- },
- {
- "field_name": "同步状态",
- "value": [{ "value_type": "text", "value": "已提交 CRM 同步" }]
- },
- {
- "field_name": "同步是否成功",
- "value": [{ "value_type": "ref", "value": "$.step_call_crm_api.body.success" }]
- },
- {
- "field_name": "同步结果说明",
- "value": [{ "value_type": "ref", "value": "$.step_call_crm_api.body.message" }]
- },
- {
- "field_name": "备注",
- "value": [{ "value_type": "text", "value": "由按钮触发自动发起同步请求" }]
- }
- ]
- }
- }
- ]
-}
-```
-
-**关键点**:
-- `ButtonTrigger` 适合“人工确认后再执行”的场景,比如同步 CRM、推送 ERP、发起审批等
-- `button_type: "buttonField"` 表示按钮挂在记录上,因此可以直接引用当前记录的字段和值
-- `HTTPClientAction.raw_body` 可以通过 `text + ref + text` 的方式动态拼接 JSON 请求体
-- `HTTPClientAction` 的输出引用规则是:`response_type=none` 时不可引用;`response_type=text` 时只能用 `$.stepId` 引整个文本;`response_type=json` 时用 `$.stepId.body` 引整个 body、用 `$.stepId.body.字段名` 引 body 中字段,同时 `$.stepId.status_code` 表示 HTTP 返回状态码
-- `HTTPClientAction.response_value` 中声明了哪些字段,后续节点就只能引用这些字段;例如 `$.step_call_crm_api.body.success`、`$.step_call_crm_api.body.message`
-- `AddRecordAction` 常用于写日志表、操作审计表、同步结果表,便于追踪谁在什么时候触发了外部调用
-- 示例里的 `fldLeadName` / `fldMobile` / `fldCompany` / `fldOwner` 只是占位的 fieldId,请以实际表字段 ID 为准
-
----
-
-## 构造技巧
-
-### Loop 构造要点
-
-1. **数据源**: `Loop.data` 必须传入 `ref` 类型,通常是 `FindRecordAction` 的 `fieldRecords`
-2. **循环体**: `children.links` 必须包含 `kind: "loop_start"` 指向循环体入口
-3. **引用**: 循环体内用 `$.{loopStepId}.item.{fieldId}` 引用当前元素
-4. **索引**: 用 `$.{loopStepId}.index` 获取当前索引(从 0 开始)
-
-### 分支构造要点
-
-1. **IfElseBranch**:
- - 适合二元判断(是/否、大于/小于)
- - `children.links` 必须包含 `if_true` 和 `if_false`
- - 可以用 `next` 指向汇合点
-
-2. **SwitchBranch**:
- - 适合多路分类(3路及以上)
- - `label` 对应 `child_branch_list` 中的条件顺序
- - 建议加一个兜底分支(其他)
-
-### 字段值构造
-
-| 字段类型 | value_type | 示例 |
-|---------|------------|------|
-| 文本 | `text` | `{"value_type": "text", "value": "张三"}` |
-| 数字 | `number` | `{"value_type": "number", "value": 100}` |
-| 单选 | `option` | `{"value_type": "option", "value": {"name": "已完成"}}` |
-| 人员 | `user` | `{"value_type": "user", "value": {"id": "ou_xxxx"}}` |
-| 引用 | `ref` | `{"value_type": "ref", "value": "$.step_1.fldxxx"}` |
-
----
-
-## 常见错误避免
-
-### Top 10 高频错误
-
-| # | 错误信息 | 原因 | 解决方案 |
-|---|---------|------|---------|
-| 1 | `path "xxx" does not exist in the output path tree` | ref 引用路径错误或 stepId 不存在 | 检查 stepId 是否在 steps 数组中;使用 fieldId 而非字段名;确保路径以 `$.` 开头 |
-| 2 | `recordInfo.conditions must be non-empty` | `condition_list` 为空数组 `[]` | 改用 `null` 或省略该字段 |
-| 3 | `At least one of filter info and ref info is required` | SetRecordAction/FindRecordAction 缺少定位条件 | 必须提供 `filter_info` 或 `ref_info` 之一 |
-| 4 | `client token is empty` | 缺少 `client_token` | 每次请求传入唯一值(时间戳或随机字符串) |
-| 5 | `valueType 'text' not allowed for fieldType '3'` | select 类型字段值格式错误 | 改用 `option` 类型 |
-| 6 | `Undefined Step Type` | 使用了不支持的 StepType | 使用 `AddRecordTrigger` 而非 `CreateRecordTrigger` |
-| 7 | `prompt references an unknown reference from step` | 引用的 stepId 不存在 | 确保引用的 step 在同一 workflow 的 steps 数组中 |
-| 8 | `[2200] Internal Error` | 1. steps[].id 重复 2. next/children.links 引用了不存在的 step | 确保所有 step id 唯一;检查引用关系 |
-| 9 | 工作流结构不完整 | Branch/Loop 节点缺少 `children` | 仅 Branch(IfElseBranch/SwitchBranch)和 Loop 节点需要 `children`,Trigger/Action 节点无需设置 |
-| 10 | 嵌套分支过于复杂 | 多层 IfElseBranch 嵌套 | 3+ 路分支用 SwitchBranch 替代嵌套 IfElseBranch |
-
-### 其他常见错误
-
-**1. condition_list 为空数组**
-```json
-// ❌ 错误
-{ "condition_list": [] }
-
-// ✅ 正确
-{ "condition_list": null }
-// 或省略该字段
-```
-
-**2. filter_info 和 ref_info 同时提供**
-```json
-// ❌ 错误
-{ "filter_info": {...}, "ref_info": {...} }
-
-// ✅ 正确(二选一)
-{ "filter_info": {...}, "ref_info": null }
-{ "filter_info": null, "ref_info": {...} }
-```
-
-**3. 使用字段名而非 fieldId**
-```json
-// ❌ 错误
-{ "value": "$.step_1.客户名称" }
-
-// ✅ 正确
-{ "value": "$.step_1.fldXXXXXXXX" }
-```
-
----
+| 错误 | 处理 |
+|---|---|
+| 查询/启停也读 schema | 停下,直接用 `+workflow-list/get/enable/disable` |
+| 为多个可能命令批量看 help | 只看当前报错或即将执行的一个命令 |
+| 把字段名当 field ID 写入 ref | 先 `+field-list --compact`,ref 下钻优先用 field ID |
+| 分支/循环没有 `children.links` | 按 branch/loop schema 补 `if_true/if_false/case/loop_start` |
+| SetRecordAction/FindRecordAction 缺定位条件 | 提供 `filter_info` 或 `ref_info` |
+| HTTPClientAction 后续节点引用不到字段 | `response_type: "json"` 时填写 `response_value` 声明输出字段 |
+| Loop 内引用错路径 | 用 `$.{loopStepId}.item.{fieldId}` 和 `$.{loopStepId}.index` |
## 参考
-- [lark-base-workflow-schema.md](lark-base-workflow-schema.md) — 字段定义参考
-- 创建/更新前先确认真实表名、字段名和目标 workflow ID;`steps` 结构按 schema 构造,不凭自然语言猜 `type`
+- [lark-base-workflow-schema.md](lark-base-workflow-schema.md):step type 路由和基础结构。
+- [workflow-steps/common-types-and-refs.md](workflow-steps/common-types-and-refs.md):ValueInfo、ref、Condition、节点输出;只有构造这些细节时才读。
diff --git a/skills/lark-base/references/lark-base-workflow-schema.md b/skills/lark-base/references/lark-base-workflow-schema.md
index 916f53f6..041893cb 100644
--- a/skills/lark-base/references/lark-base-workflow-schema.md
+++ b/skills/lark-base/references/lark-base-workflow-schema.md
@@ -1,28 +1,16 @@
# Workflow steps JSON SSOT
-本文档是 Workflow `steps` JSON 的单一事实来源(SSOT),定义完整数据结构,适用于:
-- **查询场景**:理解 `+workflow-get` 返回的 `steps` 结构
-- **创建/修改场景**:构造 `+workflow-create` / `+workflow-update` 的 `--json` body
-> 💡 **本文档是纯字段参考**。如需**创建/修改**工作流的完整示例,请阅读 [workflow-guide.md](lark-base-workflow-guide.md)。
----
-## 📖 快速导航
+本文档是 Workflow steps 的按需读取入口。先读 [lark-base-workflow-guide.md](lark-base-workflow-guide.md) 确定任务路径;只有需要具体 step 字段时,再按 type 打开对应小文件。
-根据你的需求跳转到对应章节:
+## 读取顺序
-| 需求 | 章节 |
-|------|------|
-| 了解 Step 基础结构 | [WorkflowStep 基础结构](#workflowstep-基础结构) |
-| 查询 Trigger 类型及 data 字段 | [Trigger data](#trigger-data-详细结构) |
-| 查询 Action 类型及 data 字段 | [Action data](#action-data-详细结构) |
-| 查询 Branch/Loop 结构 | [Branch data](#branch-data-详细结构) / [System data](#system-data-详细结构) |
-| 查询 ValueInfo/Condition 等公共类型 | [公共类型](#公共类型) |
-
----
+1. 查询、启停 workflow:只用 `+workflow-list/get/enable/disable` 和命令返回,不读本目录,也不要默认看 help。
+2. 创建或更新 workflow:先读 guide;如果 guide 的场景表不足以构造 step,再读本文件的基础结构和 step 路由表。
+3. 先确定本次 workflow 会用到的完整 step type 集合,去重后一次性打开对应 step md 文件;不要每确定一个节点就读一次文件。
+4. 需要 value/ref/filter 条件时,把 [common-types-and-refs.md](workflow-steps/common-types-and-refs.md) 加入同一批读取;不需要这些结构时不要读。
## WorkflowStep 基础结构
-每个步骤(Trigger / Action / Branch / System)共享以下字段:
-
```json
{
"id": "step_xxx",
@@ -34,1038 +22,77 @@
```
| 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| `id` | string | 是 | 步骤唯一 ID(用户自定义,被 `next` 和 `children.links[].to` 引用) |
-| `type` | string | 是 | 步骤类型,见下方枚举 |
+|---|---|---|---|
+| `id` | string | 是 | 步骤唯一 ID,被 `next` 和 `children.links[].to` 引用 |
+| `type` | string | 是 | 步骤类型,按下方路由打开对应文件 |
| `title` | string | 否 | 步骤标题 |
-| `children` | StepChildren | 否 | 子关系边,承担所有分支/循环 |
-| `next` | string | null | 否 | 线性后继节点 ID;`null` 表示流程结束 |
-| `data` | object | 是 | 步骤详细配置,按 `type` 区分,见后续各节 |
+| `children` | StepChildren | 否 | 分支/循环关系边;普通 trigger/action 不设置 |
+| `next` | string/null | 否 | 线性后继节点 ID;`null` 表示流程结束 |
+| `data` | object | 是 | 按 `type` 区分的配置对象 |
-> **总原则**:连线写 `children`,扩展标识写 `meta`,输入参数写 `data`。
-
----
+总原则:连线写 `children`,扩展标识写 `meta`,输入参数写 `data`。
## StepChildren 与 ChildLink
-### StepChildren
-
```json
{
- "links": [ /* ChildLink[] */ ]
+ "links": [
+ { "kind": "if_true", "to": "step_4", "label": "branch_1", "desc": "金额大于1000" }
+ ]
}
```
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| `links` | ChildLink[] | 子关系边列表;无子关系时为空数组 `[]` |
-
-### ChildLink
-
-每条关系边描述从当前节点到目标节点的有向连线:
-
-```json
-{ "kind": "if_true", "to": "step_4", "label": "branch_1", "desc": "金额大于1000" }
-```
-
-| 字段 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| `kind` | string | 是 | 关系类型:`if_true` / `if_false` / `case` / `loop_start` / `slot` |
-| `to` | string | 是 | 目标节点 ID |
-| `label` | string | 否 | 可选标签(如 `branch_1`、`tool`、`llm`、`memory`) |
-| `desc` | string | 否 | 可选语义说明(如"销售部门"、"积极情绪") |
-
-`kind` 使用场景:
-
| kind | 使用节点 | 说明 |
-|------|---------|------|
+|---|---|---|
| `if_true` | IfElseBranch | 条件为真时跳转 |
| `if_false` | IfElseBranch | 条件为假时跳转 |
-| `case` | SwitchBranch / AIClassificationBranch | 多路分支,`label` 建议用 `branch_1` 等中性标签,`desc` 写语义 |
+| `case` | SwitchBranch | 多路分支,`label` 建议用 `branch_1` 等中性标签,`desc` 写语义 |
| `loop_start` | Loop | 循环体入口 |
| `slot` | AIAgentAction | 挂载 LLM / 工具 / 记忆子节点,`label` 为 `llm` / `tool` / `memory` |
----
+## Step type 路由
-## StepType 枚举
+### Trigger
-### Trigger 类型
+| type | 说明 | 按需读取 |
+|---|---|---|
+| `AddRecordTrigger` | 新增记录时触发 | [trigger-add-record.md](workflow-steps/trigger-add-record.md) |
+| `SetRecordTrigger` | 记录被修改时触发 | [trigger-set-record.md](workflow-steps/trigger-set-record.md) |
+| `ChangeRecordTrigger` | 记录满足条件时触发;新增或修改都触发 | [trigger-change-record.md](workflow-steps/trigger-change-record.md) |
+| `TimerTrigger` | 定时触发 | [trigger-timer.md](workflow-steps/trigger-timer.md) |
+| `ReminderTrigger` | 日期提醒触发 | [trigger-reminder.md](workflow-steps/trigger-reminder.md) |
+| `ButtonTrigger` | 按钮点击触发 | [trigger-button.md](workflow-steps/trigger-button.md) |
+| `LarkMessageTrigger` | 接收飞书消息触发 | [trigger-lark-message.md](workflow-steps/trigger-lark-message.md) |
-| type | 说明 |
-|------|------|
-| `AddRecordTrigger` | 新增记录时触发 |
-| `SetRecordTrigger` | 记录被修改时触发 |
-| `ChangeRecordTrigger` | 记录满足条件时触发 |
-| `TimerTrigger` | 定时触发 |
-| `ReminderTrigger` | 日期提醒触发 |
-| `ButtonTrigger` | 按钮点击触发 |
-| `LarkMessageTrigger` | 接收飞书消息触发 |
+触发器选型:新增记录用 `AddRecordTrigger`;只监听修改用 `SetRecordTrigger`;新增或修改都触发、或拿不准时用 `ChangeRecordTrigger`。"新增或修改满足同一条件就触发"(如"改为 X 或新增 X 时通知")是单个 `ChangeRecordTrigger` 的典型场景,不要拆成两条工作流。
-> 所有 Trigger 节点**请勿设置** `children` ,通过 `next` 串联后继。
+### Action
-### 触发器选型指南
+| type | 说明 | 按需读取 |
+|---|---|---|
+| `AddRecordAction` | 新增记录 | [action-add-record.md](workflow-steps/action-add-record.md) |
+| `SetRecordAction` | 更新记录 | [action-set-record.md](workflow-steps/action-set-record.md) |
+| `FindRecordAction` | 查找记录 | [action-find-record.md](workflow-steps/action-find-record.md) |
+| `HTTPClientAction` | HTTP 请求 | [action-http-client.md](workflow-steps/action-http-client.md) |
+| `Delay` | 延迟 | [action-delay.md](workflow-steps/action-delay.md) |
+| `LarkMessageAction` | 发送飞书消息 | [action-lark-message.md](workflow-steps/action-lark-message.md) |
+| `GenerateAiTextAction` | AI 生成文本 | [action-generate-ai-text.md](workflow-steps/action-generate-ai-text.md) |
-| 需求描述 | 触发器 |
-|---------|--------|
-| 新增记录时 | `AddRecordTrigger` |
-| 字段变为特定值时(**仅修改**) | `SetRecordTrigger` |
-| **新增或修改**都触发 | `ChangeRecordTrigger` |
-| 拿不准用哪个 | `ChangeRecordTrigger` |
+所有 Action 节点不要设置 `children`,通过 `next` 串联后继。
-> ⚠️ `SetRecordTrigger` 仅监听修改,`ChangeRecordTrigger` 同时监听新增 + 修改。
+### Branch / System
-### Action 类型
+| type | 说明 | 按需读取 |
+|---|---|---|
+| `IfElseBranch` | 条件分支,`children.links` 含 `if_true` 和 `if_false` | [branch-if-else.md](workflow-steps/branch-if-else.md) |
+| `SwitchBranch` | 多路分支,`children.links` 含多个 `case` | [branch-switch.md](workflow-steps/branch-switch.md) |
+| `Loop` | 循环,`children.links` 含 `loop_start` 指向循环体入口 | [system-loop.md](workflow-steps/system-loop.md) |
-| type | 说明 |
-|------|------|
-| `AddRecordAction` | 新增记录 |
-| `SetRecordAction` | 更新记录 |
-| `FindRecordAction` | 查找记录 |
-| `HTTPClientAction` | HTTP 请求 |
-| `Delay` | 延迟 |
-| `LarkMessageAction` | 发送飞书消息 |
-| `GenerateAiTextAction` | AI 生成文本 |
+## 公共结构
-> 所有 Action 节点**请勿设置** `children` ,通过 `next` 串联后继。
-
-### Branch 类型
-
-| type | 说明 |
-|------|------|
-| `IfElseBranch` | 条件分支,`children.links` 含 `if_true` 和 `if_false` |
-| `SwitchBranch` | 多路分支,`children.links` 含多个 `case` |
-
-### System 类型
-
-| type | 说明 |
-|------|------|
-| `Loop` | 循环,`children.links` 含 `loop_start` 指向循环体入口 |
-
----
-
-## Trigger data 详细结构
-
-
-### AddRecordTrigger
-
-```json
-{
- "table_name": "订单表",
- "watched_field_name": "状态",
- "trigger_control_list": ["pasteUpdate", "automationBatchUpdate"],
- "condition_list": [] /* AndCondition 数组 */
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `table_name` | 是 | 监控的数据表名 |
-| `watched_field_name` | 是 | 监控的字段名 |
-| `trigger_control_list` | 否 | 触发控制,可选值:`pasteUpdate` / `automationBatchUpdate` / `syncUpdate` / `appendImport` / `openAPIBatchUpdate` |
-| `condition_list` | 否 | 过滤条件数组,数组中每个元素为 AndCondition 结构,多个 AndCondition 之间为 OR 关系 |
-
-### ChangeRecordTrigger
-
-```json
-{
- "table_name": "任务表",
- "trigger_control_list": [],
- "condition": null
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `table_name` | 是 | 监控的数据表名 |
-| `trigger_control_list` | 否 | 触发控制,可选值:`pasteUpdate` / `automationBatchUpdate` / `syncUpdate` / `appendImport` |
-| `condition_list` | 否 | 过滤条件数组,数组中每个元素为 AndCondition 结构,多个 AndCondition 之间为 OR 关系 |
-
-### SetRecordTrigger
-
-```json
-{
- "table_name": "订单表",
- "record_watch_conjunction": "and",
- "record_watch_info": [ /* FieldCondition[] */ ],
- "field_watch_info": [
- { "field_name": "状态", "operator": "is", "value": [{ "value_type": "text", "value": "已发货" }] }
- ],
- "trigger_control_list": [],
- "condition_list": null
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|----|------|
-| `table_name` | 是 | 监控的数据表名 |
-| `record_watch_conjunction` | 否 | 记录筛选组合方式:`and` / `or`,默认 `and` |
-| `record_watch_info` | 否 | 记录级过滤条件(修改前值匹配),为空则监听全部 |
-| `field_watch_info` | 是 | 字段级监控条件列表,至少一个 |
-| `trigger_control_list` | 否 | 触发控制,可选值:`pasteUpdate` / `automationBatchUpdate` / `syncUpdate` / `appendImport` |
-| `condition_list` | 否 | 过滤条件数组,数组中每个元素为 AndCondition 结构,多个 AndCondition 之间为 OR 关系 |
-
-`FieldWatchItem`:
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| `field_name` | string | 监听字段名称 |
-| `operator` | string | 操作符(仅明确要求字段满足条件时填) |
-| `value` | ValueInfo[] | 触发值 |
-
-### TimerTrigger
-
-```json
-{
- "rule": "WEEKLY",
- "start_time": "2025-01-01 09:00",
- "sub_unit": [1, 3, 5],
- "is_never_end": true
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `rule` | 是 | `NO_REPEAT` / `DAILY` / `WEEKLY` / `MONTHLY` / `YEARLY` / `WORKDAY` / `CUSTOM` |
-| `start_time` | 否 | 开始时间,格式 `yyyy-MM-dd HH:mm` |
-| `interval` | 否 | 自定义间隔 [1,30](仅 CUSTOM) |
-| `unit` | 否 | 自定义单位:`SECOND` / `MINUTE` / `HOUR` / `DAY` / `WEEK` / `MONTH` / `YEAR` |
-| `sub_unit` | 否 | 子单位(`WEEKLY` 时为星期几数组 0-6,`MONTHLY` 时为几号数组 1-31) |
-| `end_time` | 否 | 结束时间 |
-| `is_never_end` | 否 | 是否永不结束 |
-
-### ReminderTrigger
-
-```json
-{
- "table_name": "项目表",
- "field_name": "截止日期",
- "offset": 1,
- "unit": "DAY",
- "hour": 9,
- "minute": 0,
- "condition_list": null
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `table_name` | 是 | 数据表名 |
-| `field_name` | 是 | 日期字段名(必须为 `datetime` / `created_at` / `formula` / `lookup` 类型) |
-| `unit` | 是 | 偏移单位:`MINUTE` / `HOUR` / `DAY` / `WEEK` / `MONTH` |
-| `offset` | 是 | 提前/延后的偏移量(正数=提前,负数=延后;范围由 `unit` 决定):`MINUTE` ∈ {0, 5, 15, 30, -5, -15, -30};`HOUR` ∈ [-6, -1] ∪ [1, 6];`DAY` ∈ [-7, 7];`WEEK` ∈ [-7, -1] ∪ [1, 7];`MONTH` ∈ [-7, -1] ∪ [1, 7] |
-| `hour` | 是 | 触发小时 (0-23),默认 9 |
-| `minute` | 是 | 触发分钟 (0-59),默认 0 |
-| `condition_list` | 否 | 过滤条件数组,数组中每个元素为 AndCondition 结构,多个 AndCondition 之间为 OR 关系 |
-
-
-### ButtonTrigger
-
-```json
-{
- "button_type": "buttonField",
- "table_name": "审批表"
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `button_type` | 是 | 按钮类型:`buttonField`(表格里的按钮,可操作当前记录数据)/ `buttonElement`(仪表盘、应用页面上的按钮,可执行整体操作) |
-| `table_name` | 否 | 绑定的数据表名,仅 `button_type=buttonField` 时填写 |
-
-> `buttonField` 和 `buttonElement` 的输出能力不同,详见下方「ButtonTrigger(按钮触发器)」输出说明。
-
-
-### LarkMessageTrigger
-
-```json
-{
- "receive_scene": "group",
- "receiver": [{ "value_type": "group", "value": {"id": "oc_xxxx", "name": "测试群"} }],
- "scope": "all",
- "filter": {
- "conjunction": "and",
- "content_contains": ["关键词"],
- "sender_contains": [{ "value_type": "user", "value": {"id": "ou_xxxx", "name": ""} }],
- "is_new_message": true,
- "is_message_contain_attachment": false
- }
-}
-```
-
-| 字段 | 必填 | 说明|
-|------|------|---|
-| `receive_scene` | 是 | 接收场景:`group`(群聊)/ `chat`(单聊)|
-| `receiver` | 是 | 触发来源,支持 `user` / `group` / `ref`。在单聊场景下,该字段指“可以和机器人单聊的用户”;在群聊场景下,该字段指“接收信息的群组”|
-| `scope` | 是 | 触发范围:`at`(@提及)/ `all`(所有消息)。该参数仅在群聊场景有效,单聊场景请勿指定该参数|
-| `filter` | 是 | MessageFilter 消息过滤条件|
-
-`MessageFilter`:
-
-| 字段 | 类型 | 说明 |
-|------|------|----|
-| `conjunction` | string | `and` 满足所有条件 / `or` 任一条件|
-| `content_contains` | string[] | 关键词列表|
-| `sender_contains` | ValueInfo[] | 筛选发送人(仅群聊+群组来源时生效,单聊场景请勿指定该参数)|
-| `is_new_message` | boolean | 仅新话题消息(仅群聊时有效,单聊场景请勿指定该参数)|
-| `is_message_contain_attachment` | boolean | 是否仅附件消息触发|
-
-## Action data 详细结构
-
-### AddRecordAction
-
-```json
-{
- "table_name": "订单表",
- "field_values": [
- { "field_name": "客户名称", "value": [{ "value_type": "text", "value": "张三" }] },
- { "field_name": "金额", "value": [{ "value_type": "number", "value": 100 }] },
- { "field_name": "创建人", "value": [{ "value_type": "ref", "value": "$.trigger_1.fieldIdxxx" }] }
- ]
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `table_name` | 是 | 目标数据表名 |
-| `field_values` | 是 | RecordFieldValue[] |
-
-### SetRecordAction
-
-```json
-{
- "table_name": "订单表",
- "max_set_record_num": 10,
- "field_values": [
- { "field_name": "状态", "value": [{ "value_type": "option", "value": { "id": "opt1", "name": "已完成" } }] }
- ],
- "filter_info": { /* RecordFilterInfo */ },
- "ref_info": { "step_id": "step_trigger" }
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `table_name` | 是 | 目标数据表名 |
-| `max_set_record_num` | 否 | 最大更新记录数,默认 100,范围 1-15000 |
-| `field_values` | 是 | RecordFieldValue[] |
-| `filter_info` | 否* | RecordFilterInfo 过滤条件(与 `ref_info` 互斥) |
-| `ref_info` | 否* | RefInfo 引用前置步骤的记录(与 `filter_info` 互斥) |
-
-### FindRecordAction
-
-```json
-{
- "table_name": "客户表",
- "field_names": ["客户名称", "联系方式", "等级"],
- "should_proceed_when_no_results": true,
- "filter_info": { /* RecordFilterInfo */ }
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `table_name` | 是 | 目标数据表名 |
-| `field_names` | 是 | 要检索的字段名列表,至少一个 |
-| `should_proceed_when_no_results` | 否 | 无结果时是否继续后续步骤,默认 `true` |
-| `filter_info` | 否* | RecordFilterInfo(与 `ref_info` 互斥) |
-| `ref_info` | 否* | RefInfo(与 `filter_info` 互斥) |
-
-### HTTPClientAction
-
-```json
-{
- "method": "POST",
- "url": [{ "value_type": "text", "value": "https://api.example.com/webhook" }],
- "queries": [
- { "key": "source", "value": [{ "value_type": "text", "value": "workflow" }] }
- ],
- "headers": [
- { "key": "Content-Type", "value": [{ "value_type": "text", "value": "application/json" }] }
- ],
- "body_type": "raw",
- "raw_body": [
- { "value_type": "text", "value": "{\"record_id\":\"" },
- { "value_type": "ref", "value": "$.step_1.recordId" },
- { "value_type": "text", "value": "\"}" }
- ],
- "response_type": "json",
- "response_value": "{\"success\":true,\"message\":\"data fetched successfully\"}"
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|-----|------|
-| `method` | 否 | 请求方法:`GET` / `POST` / `PUT` / `PATCH` / `DELETE`,默认 `POST` |
-| `url` | 是 | ValueInfo[],请求 URL,支持 `text` / `ref` 拼接 |
-| `queries` | 否 | KeyValue[],查询参数 |
-| `headers` | 否 | KeyValue[],请求头 |
-| `body_type` | 否 | 请求体类型:`none` / `raw` / `form-data` / `form-urlencoded`,默认 `raw` |
-| `raw_body` | 否 | ValueInfo[],原始请求体,仅 `body_type=raw` 时使用 |
-| `form_body` | 否 | KeyValue[],表单数据,仅 `body_type=form-data` 或 `body_type=form-urlencoded` 时使用 |
-| `response_type` | 否 | 响应类型:`none` / `text` / `json`,默认 `json` |
-| `response_value` | 否 | string,JSON 字符串形式的响应结果示例;仅当 `response_type=json` 时必填 |
-
-`KeyValue`:
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| `key` | string | 参数名 / 请求头名 |
-| `value` | ValueInfo[] | 参数值 / 请求头值,支持 `text` / `ref` |
-
-### Delay
-
-```json
-{ "duration": 30 }
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `duration` | 是 | 延迟时长(分钟),范围 [1, 120] |
-
-### LarkMessageAction
-
-```json
-{
- "receiver": [{ "value_type": "user", "value": {"id": "ou_xxxx"} }],
- "send_to_everyone": false,
- "title": [{ "value_type": "text", "value": "新订单通知" }],
- "content": [
- { "value_type": "text", "value": "客户 " },
- { "value_type": "ref", "value": "$.trigger_1.fldCustomerName" },
- { "value_type": "text", "value": " 创建了新订单" }
- ],
- "btn_list": [
- { "text": "查看详情", "btn_action": "openLink", "link": [{ "value_type": "text", "value": "https://example.com" }] }
- ]
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `receiver` | 是 | ValueInfo[] |
-| `send_to_everyone` | 是 | 是否发送给所有人 |
-| `title` | 否 | TextRefItem[] 消息标题 |
-| `content` | 是 | TextRefItem[] 消息内容 |
-| `btn_list` | 是 | 按钮列表,不需要时为空数组 |
-
-`ButtonConfig`:
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| `text` | string | 按钮文字 |
-| `btn_action` | string | `addRecord` / `setRecord` / `openLink` |
-| `link` | ValueInfo[] | 跳转链接(`openLink` 时使用) |
-| `table_name` | string | 操作表名(`addRecord` 时使用) |
-| `record_values` | RecordFieldValue[] | 记录赋值(`addRecord` / `setRecord` 时使用) |
-
-### GenerateAiTextAction
-
-```json
-{
- "prompt": [
- { "value_type": "text", "value": "请总结以下内容:" },
- { "value_type": "ref", "value": "$.step_1.fieldxxx" }
- ]
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `prompt` | 是 | TextRefItem[] 提示词,支持 `text` / `ref` |
-
-
-## Branch data 详细结构
-
-### IfElseBranch
-
-`children.links` 包含 `if_true` 和 `if_false` 两条边,`next` 指向两个分支汇合后的后继节点。
-
-**如果涉及到复杂的多分支场景(分支数目 >= 3时),你应该采用 SwitchBranch,而不是嵌套的 IfElseBranch**
-
-```json
-{
- "condition": {
- "conjunction": "or",
- "conditions": [
- {
- "conjunction": "and",
- "conditions": [
- {
- "left_value": { "value_type": "ref", "value": "$.step_1.fieldxxx" },
- "operator": "isGreater",
- "right_value": [{ "value_type": "number", "value": 1000 }]
- }
- ]
- }
- ]
- }
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `condition` | 是 | OrGroup 判断条件,结构为 `(A and B) or (C and D)` |
-
-### SwitchBranch
-
-`children.links` 包含多个 `case` 边(`label` 建议用 `branch_1`、`branch_2`,语义写在 `desc`)。
-
-```json
-{
- "mode": "exclusive",
- "no_match_action": "classifyToOther",
- "child_branch_list": [
- {
- "name": "高优先级",
- "condition": {
- "conjunction": "or",
- "conditions": [
- {
- "conjunction": "and",
- "conditions": [
- {
- "left_value": { "value_type": "ref", "value": "$.step_1.fieldxxx" },
- "operator": "is",
- "right_value": [{ "value_type": "text", "value": "P0" }]
- }
- ]
- }
- ]
- }
- }
- ]
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `mode` | 否 | 分支模式。`exclusive`:排他模式,仅执行一个满足条件的子分支;`parallel`:并行模式,执行所有满足条件的子分支。默认 `exclusive` |
-| `no_match_action` | 否 | `mode=exclusive` 时使用,无匹配时的处理策略。`classifyToOther`:归类到其他分支;`fail`:报错终止。默认 `classifyToOther` |
-| `fail_mode` | 否 | `mode=parallel` 时使用,部分分支出错时策略。`partialSuccess`:部分成功即继续;`fail`:任一失败即终止。默认 `partialSuccess` |
-| `match_mode` | 否 | `mode=parallel` 时使用,所有分支不满足时策略。`noneMatchSkip`:跳过继续;`noneMatchFail`:报错终止。默认 `noneMatchSkip` |
-| `child_branch_list` | 是 | BranchItem[],1-10 个条件分支 |
-
-`BranchItem`:
-
-| 字段 | 类型 | 说明 |
-|------|------|------|
-| `name` | string | 分支名称 |
-| `condition` | OrGroup | 分支条件 |
-
-
-## System data 详细结构
-
-### Loop
-
-`children.links` 包含 `loop_start` 边指向循环体入口,`next` 指向循环结束后的后继节点。
-
-```json
-{
- "loop_mode": "continue",
- "max_loop_times": 100,
- "data": [{ "value_type": "ref", "value": "$.find_record_stepIdxxx.fieldRecords" }]
-}
-```
-
-| 字段 | 必填 | 说明 |
-|------|------|------|
-| `data` | 是 | ValueInfo[](仅支持 `ref` 类型),循环数据源,只能填一个 |
-| `loop_mode` | 否 | 单次错误时是否继续:`end`(终止)/ `continue`(继续) |
-| `max_loop_times` | 否 | 最大循环次数 |
-
----
-
-
-## 公共类型
-
-### ValueInfo
-
-所有值的基础类型,通过 `value_type` 区分:
-
-| value_type | value 类型 | 说明 | 示例 |
-|------------|-----------|------|------|
-| `text` | string | 文本 | `"张三"` |
-| `number` | number | 数字 | `100` |
-| `boolean` | boolean | 布尔值 | `true` |
-| `date` | string | 日期,可以是具体时间字符串,或者相对时间值 | `"2025/01/01"`、`"2025/01/01 11:00"`、`"now"`、`"now 11:00"`、`"today"`、`"today 11:00"`、`"yesterday"`、`"yesterday 11:00"`、`"lastWeek"`、`"currentMonth"`、`"lastMonth"`、`"theLastWeek"`、`"theNextWeek"`、`"theLastMonth"`、`"theNextMonth"` |
-| `option` | `{ id, name }` | 选项 | `{ "id": "opt1", "name": "已完成" }` |
-| `link` | `{ text, link }` | 链接(含文字和 URL), 文字和 URL 的格式可以是 ValueInfo 中的 text/ref 类型 | `{ "text": [{ "value_type": "text", "value": "查看详情" }], "link": [{ "value_type": "text", "value": "https://example.com" }] }`、`{ "text": [{ "value_type": "text", "value": "查看详情" }], "link": [{ "value_type": "ref", "value": "$.step_1.fldXXX" }] }` |
-| `user` | `{ id, name }` | 用户 OpenID、名字 | `{ "id": "ou_xxxx", "name": "张三" }` |
-| `group` | `{ id, name }` | 群 Chat ID、名字 | `{ "id": "oc_xxx", "name": "测试群" }` |
-| `ref` | `string` | 引用前置节点输出的路径 | 参考 ref 引用变量详解 章节 |
-
-> ⚠️ **所有涉及用户的 value 中的 id 统一使用 OpenID(`ou_xxxx` 格式)**,由 CLI 层来完成转换
-> ⚠️ **所有涉及群的 value 中的 id 统一使用 ChatID(`oc_xxxx` 格式)**,由 CLI 层来完成转换
-
-### ref 引用变量详解
-
-`ref` 类型是工作流中节点间数据传递的核心机制。当 `value_type` 为 `ref` 时,`value` 指向前置节点的某个输出变量。本节详细描述每个节点可供引用的输出变量定义。
-
-#### 引用路径格式
-
-```
-$.{stepId}
-$.{stepId}.{pathId}
-$.{stepId}.{pathId}.{childPathId}
-$.{stepId}.{pathId}.{childPathId}.{grandChildPathId}
-```
-
-- `{stepId}`:前置节点的 `id`(即 WorkflowStep 中的 `id` 字段)
-- `{pathId}`:节点输出的路径标识符
-- 支持多层下钻,如引用字段的属性:`$.step_1.fldXXX.name`
-
----
-
-#### 触发器节点输出
-
-##### 记录触发器(AddRecordTrigger / ChangeRecordTrigger / SetRecordTrigger / ReminderTrigger)
-
-这 4 个触发器的输出结构完全一致:
-
-| pathId | 说明 | 引用示例 |
-|--------|------|----------|
-| `{fieldId}` | 字段id,从配置表的所有字段或者指定字段id生成,可下钻字段属性 | `$.{stepId}.{fieldId}` |
-| `{fieldId}.fieldId` | 字段id属性 | `$.{stepId}.{fieldId}.fieldId` |
-| `{fieldId}.fieldName` | 字段名属性 | `$.{stepId}.{fieldId}.fieldName` |
-| `startTime` | 触发时间戳 | `$.{stepId}.startTime` |
-| `recordId` | 记录 ID | `$.{stepId}.recordId` |
-| `recordLink` | 记录链接 | `$.{stepId}.recordLink` |
-| `recordCreatedUser` | 记录创建者 | `$.{stepId}.recordCreatedUser` |
-| `recordCreatedTime` | 记录创建时间 | `$.{stepId}.recordCreatedTime` |
-| `recordModifiedUser` | 最后修改者 | `$.{stepId}.recordModifiedUser` |
-| `recordModifiedTime` | 最后修改时间 | `$.{stepId}.recordModifiedTime` |
-
-**动态字段输出规则**:
-
-- 读取触发器所配置的数据表的所有字段
-- 每个字段生成一条输出:`pathId` = fieldId
-- 若字段为关联字段,children 为关联表所有字段(单层下钻,不再递归)
-- 每个字段可下钻特定的字段属性(见「字段属性下钻」)
-
-**recordLink 的 children**:如果配置了数据表,则为该表所有视图的列表,每个视图 `{ pathId: viewId, pathName: viewName, pathType: 'string' }`。引用示例:`$.{stepId}.recordLink.{viewId}`。
-
-##### ButtonTrigger(按钮触发器)
-
-`ButtonTrigger` 的输出取决于 `button_type`:
-
-#### `button_type = buttonField`
-
-| pathId | 说明 | 引用示例 |
-|--------|------|----------|
-| `{fieldId}` | 字段id,从配置表的所有字段或者指定字段id生成,可下钻字段属性 | `$.{stepId}.{fieldId}` |
-| `{fieldId}.fieldId` | 字段id属性 | `$.{stepId}.{fieldId}.fieldId` |
-| `{fieldId}.fieldName` | 字段名属性 | `$.{stepId}.{fieldId}.fieldName` |
-| `recordId` | 记录 ID | `$.{stepId}.recordId` |
-| `recordLink` | 记录链接 | `$.{stepId}.recordLink` |
-| `recordCreatedUser` | 记录创建者 | `$.{stepId}.recordCreatedUser` |
-| `recordModifiedUser` | 最后修改者 | `$.{stepId}.recordModifiedUser` |
-| `recordModifiedTime` | 最后修改时间 | `$.{stepId}.recordModifiedTime` |
-| `time` | 触发时间 | `$.{stepId}.time` |
-| `user` | 触发人 | `$.{stepId}.user` |
-| `buttonName` | 触发的按钮名称 | `$.{stepId}.buttonName` |
-
-#### `button_type = buttonElement`
-
-| pathId | 说明 | 引用示例 |
-|--------|------|----------|
-| `time` | 触发时间 | `$.{stepId}.time` |
-| `user` | 触发人 | `$.{stepId}.user` |
-| `buttonName` | 触发的按钮名称 | `$.{stepId}.buttonName` |
-
-##### TimerTrigger(定时触发器)
-
-| pathId | 说明 | 引用示例 |
-|--------|------|----------|
-| `scheduleTime` | 定时触发时间 | `$.{stepId}.scheduleTime` |
-
-##### LarkMessageTrigger(飞书消息触发器)
-
-| pathId | 说明 | 引用示例 |
-|--------|------|----------|
-| `Sender` | 消息发送者 | `$.{stepId}.Sender` |
-| `AtUser` | 消息中被@的用户 | `$.{stepId}.AtUser` |
-| `SenderGroup` | 消息所在群(仅群聊场景) | `$.{stepId}.SenderGroup` |
-| `MessageSendTime` | 消息发送时间 | `$.{stepId}.MessageSendTime` |
-| `MessageContent` | 消息正文 | `$.{stepId}.MessageContent` |
-| `MessageType` | 消息类型标识 | `$.{stepId}.MessageType` |
-| `MessageID` | 消息唯一标识 | `$.{stepId}.MessageID` |
-| `MessageLink` | 消息链接(仅群聊场景) | `$.{stepId}.MessageLink` |
-| `ParentID` | 回复的消息 ID | `$.{stepId}.ParentID` |
-| `ThreadID` | 所在话题消息 ID | `$.{stepId}.ThreadID` |
-| `Attachments` | 消息中的附件 | `$.{stepId}.Attachments` |
-
-条件限制:
-
-- 若场景为单聊(`receive_scene = "Chat"`),则 `SenderGroup` 和 `MessageLink` 不可用
-
----
-
-#### 操作节点输出
-
-##### FindRecordAction(查找记录)
-
-| pathId | 说明 | 引用示例|
-|--------|------|-------|
-| `fieldRecords` | 所有找到的记录的引用(可用于 Loop 遍历) | `$.{stepId}.fieldRecords`|
-| `firstfieldsRecord` | 第一条匹配记录 | `$.{stepId}.firstfieldsRecord`|
-| `firstfieldsRecord.{fieldId}` | 首条记录的字段值,可下钻字段属性 | `$.{stepId}.firstfieldsRecord.{fieldId}`|
-| `firstfieldsRecord.recordId` | 记录 ID 数组 | `$.{stepId}.firstfieldsRecord.recordId`|
-| `fields` | 查找到的所有记录某列值 | 不支持引用|
-| `fields.{fieldId}` | 用户选择的字段 | `$.{stepId}.fields.{fieldId}`|
-| `fields.{fieldId}.fieldId` | 用户选择的字段id数组 | `$.{stepId}.fields.{fieldId}.fieldId`|
-| `fields.{fieldId}.fieldName` | 用户选择的字段名数组 | `$.{stepId}.fields.{fieldId}.fieldName`|
-| `fields.recordId` | 记录 ID 数组 | `$.{stepId}.fields.recordId`|
-| `recordNum` | 找到记录总数 | `$.{stepId}.recordNum`|
-
-##### AddRecordAction(新增记录)
-
-| pathId | 说明 | 引用示例 |
-|--------|------|----------|
-| `{fieldId}` | 用户配置的字段值,可下钻字段属性 | `$.{stepId}.{fieldId}` |
-| `{fieldId}.fieldId` | 用户配置的字段id | `$.{stepId}.{fieldId}.fieldId` |
-| `{fieldId}.fieldName` | 用户配置的字段名 | `$.{stepId}.{fieldId}.fieldName` |
-| `recordId` | 新增的记录 ID | `$.{stepId}.recordId` |
-| `recordLink` | 新增的记录 URL | `$.{stepId}.recordLink` |
-
-##### SetRecordAction(更新记录)
-
-| pathId | 说明 | 引用示例 |
-|--------|------|----------|
-| `{fieldId}` | 用户配置的字段值,可下钻字段属性 | `$.{stepId}.{fieldId}` |
-| `{fieldId}.fieldId` | 用户配置的字段id | `$.{stepId}.{fieldId}.fieldId` |
-| `{fieldId}.fieldName` | 用户配置的字段名 | `$.{stepId}.{fieldId}.fieldName` |
-| `recordId` | 记录 ID 数组(因可能更新多条记录) | `$.{stepId}.recordId` |
-
-##### HTTPClientAction(HTTP 请求)
-
-HTTPClientAction 的输出取决于 `response_type`:
-
-| response_type | 是否可引用 | 输出说明 | 引用示例 |
-|--------------|-----------|----------|----------|
-| `none` | 否 | 无任何可引用输出 | 不支持引用 |
-| `text` | 是 | 整个响应文本作为节点整体输出 | `$.{stepId}` |
-| `json` | 是 | 响应体整体挂在 `body` 下,同时返回 `status_code`;仅可引用 `response_value` 中声明的字段 | `$.{stepId}.body`、`$.{stepId}.body.success`、`$.{stepId}.body.message`、`$.{stepId}.status_code` |
-
-**补充说明**:
-
-- 当 `response_type = none` 时,后续节点无法引用 HTTPClientAction 的任何输出
-- 当 `response_type = text` 时,`$.{stepId}` 表示整个响应文本
-- 当 `response_type = json` 时,`$.{stepId}.body` 表示整个 JSON body,`$.{stepId}.body.字段名` 表示 body 中某个字段
-- 仅当 `response_type = json` 时,`$.{stepId}.status_code` 表示请求该 HTTP URL 后返回的 HTTP 状态码
-- 仅当 `response_type = json` 时,`response_value` 必填
-- 当 `response_type = json` 时,后续节点只能引用 `response_value` 中声明过的字段
-
-**案例**:
-
-假设某个 `HTTPClientAction` 的配置如下:
-
-```json
-{
- "id": "step_http_1",
- "type": "HTTPClientAction",
- "data": {
- "response_type": "json",
- "response_value": "{\"success\":true,\"message\":\"ok\"}"
- }
-}
-```
-
-则后续节点仅可以引用:
-
-- `$.step_http_1.body`
-- `$.step_http_1.body.success`
-- `$.step_http_1.body.message`
-- `$.step_http_1.status_code`
-
-但**不能**引用未在 `response_value` 中声明的字段,例如:
-
-- `$.step_http_1.body.data`
-- `$.step_http_1.body.request_id`
-
-##### GenerateAiTextAction(AI 生成文本)
-
-| pathId | 说明 | 引用示例 |
-|--------|------|----------|
-| (整体出参) | AI 生成的文本内容(不支持下钻,只能引用 `$.{stepId}`) | `$.{stepId}` |
-
-##### 无输出的操作节点
-
-以下节点不产生任何可引用的输出数据:
-
-- **Delay**(延时等待)
-- **LarkMessageAction**(发送飞书消息)
-
----
-
-#### 分支节点输出
-
-以下分支节点均不产生任何可引用的输出数据:
-
-- **IfElseBranch**(条件分支)
-- **SwitchBranch**(多条件分支)
-
----
-
-#### 系统节点输出
-
-##### Loop(循环)
-
-| pathId | 说明 | 引用示例 |
-|--------|------|----------|
-| `item` | 当前循环元素 | `$.{stepId}.item` |
-| `index` | 从 0 开始的循环索引 | `$.{stepId}.index` |
-
-**`item` 的类型推断规则**(由循环数据源决定):
-
-**场景一:遍历组合记录** — 数据源为 `record` 类型时(如 FindRecordAction 的 `fieldRecords`),`item` 类型为 `record`,可向下选择具体字段:
-
-| 说明 | 引用示例 |
-|------|----------|
-| 当前遍历的记录(record) | `$.{loopStepId}.item` |
-| 记录的具体字段 | `$.{loopStepId}.item.{fieldId}` |
-| 从 0 开始的索引(number) | `$.{loopStepId}.index` |
-
-**场景二:遍历字段** — 数据源为某个多值类型字段时,比如附件字段、人员字段,`item` 继承该字段的类型并可继续下钻字段属性:
-
-| 说明 | 引用示例 |
-|------|----------|
-| 当前遍历的元素(类型继承数据源字段类型,例如人员字段) | `$.{loopStepId}.item` |
-| 用户姓名 | `$.{loopStepId}.item.name` |
-| 从 0 开始的索引(number) | `$.{loopStepId}.index` |
-
----
-
-#### 字段属性下钻
-
-每个字段变量都可以进一步下钻选择字段的属性。所有字段至少支持 `fieldId` 和 `fieldName` 两个基础属性,部分字段还支持额外属性:
-
-| 字段类型 | 属性名称 | 属性 pathId | 属性 pathType | 说明 |
-|----------|---------|-------------|--------------|------|
-| **所有字段(基础)** | 字段 ID | `fieldId` | `string` | 字段的唯一标识 |
-| | 字段名称 | `fieldName` | `string` | 字段的显示名称 |
-| **人员字段**(`user` / `created_by` / `updated_by`) | 姓名 | `name` | `string` | 用户姓名 |
-| **日期字段**(`datetime` / `created_at` / `updated_at`) | 时间戳 | `timestamp` | `number` | 时间戳数值 |
-| **附件字段**(`attachment`) | 文件名 | `fileName` | `string` | 附件文件名 |
-| | 文件类型 | `fileType` | `string` | MIME 类型 |
-| | 文件大小 | `size` | `number` | 文件字节数 |
-| | 文件 Token | `fileToken` | `string` | 附件 token |
-| **超链接文本字段**(`text` 且 `style.type=url`) | 文本 | `text` | `string` | 链接文本部分 |
-| | 链接 | `link` | `string` | 链接 URL 部分 |
-| **自动编号字段**(`auto_number`) | 序号 | `sequence` | `number` | 编号的纯数字序号 |
-| **关联字段**(`link`) | 字段下钻 | `{fieldId}` | - | 可下钻到关联表的字段 |
-
-> 其他字段类型(如 `text`、`number`、`checkbox`、`select`、`location`、`formula`、`lookup` 等)仅支持 `fieldId` 和 `fieldName` 两个基础属性。
-
-下钻引用示例:
-
-```
-$.{stepId}.{fieldId} → 字段值本身
-$.{stepId}.{fieldId}.fieldId → 字段 ID(string)
-$.{stepId}.{fieldId}.fieldName → 字段名称(string)
-$.{stepId}.{fieldId}.name → 人员姓名列表(array,仅人员字段)
-$.{stepId}.{fieldId}.unionId → 人员 unionId 列表(array,仅人员字段)
-$.{stepId}.{fieldId}.timestamp → 时间戳(array,仅日期字段)
-$.{stepId}.{fieldId}.fileName → 文件名列表(array,仅附件字段)
-$.{stepId}.{fieldId}.fileToken → 文件 Token 列表(array,仅附件字段)
-```
-
----
-
-#### 节点输出能力总览
-
-| 节点 | 类型 | 有输出 | 输出特性 |
-|------|------|--------|---------|
-| AddRecordTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性) |
-| ChangeRecordTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性) |
-| SetRecordTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性) |
-| ReminderTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性) |
-| ButtonTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性;buttonElement 仅基础触发属性) |
-| TimerTrigger | 触发器 | ✅ | 静态(仅 scheduleTime) |
-| LarkMessageTrigger | 触发器 | ✅ | 静态(消息属性列表) |
-| FindRecordAction | 动作 | ✅ | 动态(用户选择的字段) |
-| AddRecordAction | 动作 | ✅ | 动态(用户配置的字段) |
-| SetRecordAction | 动作 | ✅ | 动态(用户配置的字段) |
-| HTTPClientAction | 动作 | ✅ | 动态(取决于用户配置的 HTTP 响应输出) |
-| GenerateAiTextAction | 动作 | ✅ | 静态(单 string) |
-| Delay | 动作 | ❌ | 无输出 |
-| LarkMessageAction | 动作 | ❌ | 无输出 |
-| IfElseBranch | 分支 | ❌ | 无输出 |
-| SwitchBranch | 分支 | ❌ | 无输出 |
-| Loop | 系统 | ✅ | 动态(取决于数据源) |
-
----
-
-### TextRefItem
-
-文本与引用混排,用于消息内容等动态拼接场景:
-
-```json
-[
- { "value_type": "text", "value": "客户 " },
- { "value_type": "ref", "value": "$.step_1.fieldxxx" },
- { "value_type": "text", "value": " 创建了新订单" }
-]
-```
-
-### RecordFieldValue
-
-```json
-{ "field_name": "客户名称", "value": [{ "value_type": "text", "value": "张三" }] }
-```
-
-### AndCondition(Trigger 过滤条件)
-
-```json
-{
- "conjunction": "and",
- "conditions": [
- { "field_name": "状态", "operator": "is", "value": [{ "value_type": "text", "value": "进行中" }] }
- ]
-}
-```
-
-### OrGroup(Branch 分支条件)
-
-```json
-{
- "conjunction": "or",
- "conditions": [
- {
- "conjunction": "and",
- "conditions": [
- {
- "left_value": { "value_type": "ref", "value": "$.step_1.fieldxxx" },
- "operator": "isGreater",
- "right_value": [{ "value_type": "number", "value": 1000 }]
- }
- ]
- }
- ]
-}
-```
-
-**operator 可选值:** `is` / `isNot` / `containsAny` / `doesNotContainAny` / /`containsAll`/ `isEmpty` / `isNotEmpty` / `isGreater` / `isGreaterEqual` / `isLess` / `isLessEqual`
-
-### RecordFilterInfo
-** 由于 conjunction 只支持 and,若需要实现 字段X 等于 A 或 B,你可以使用 containsAny
-```json
-{
- "conjunction": "and",
- "conditions": [
- { "field_name": "状态", "operator": "is", "value": [{ "value_type": "text", "value": "进行中" }] }
- ]
-}
-```
-
-### `select` 字段多值匹配
-
-| 操作 | operator | 正确写法 |
-|------|---------|---------|
-| 等于单个值 | `is` | `[{"value_type": "option", "value": {"name": "L2"}}]` |
-| 匹配多个值(L2 或 L3) | `containsAny` | `[{"value_type": "option", "value": {"name": "L2"}}, {"value_type": "option", "value": {"name": "L3"}}]` |
-
-> ⚠️ 不要用多个 `is` 条件(会被当作 OR,无法实现 AND)。推荐使用 `containsAny` 操作符匹配多个值。
-
-> ⚠️ **Select 字段条件**:`value_type` 必须为 `option`,`value` 对象可只传 `name`(如 `{"name": "L2"}`),无需提供选项 ID。
-
-### RefInfo
-
-```json
-{ "step_id": "step_trigger" }
-```
-
----
-
-## 完整示例:条件分支 + 发送消息
-
-```json
-{
- "title": "新订单自动通知",
- "steps": [
- {
- "id": "step_1",
- "type": "AddRecordTrigger",
- "title": "当「订单表」新增记录时触发",
- "next": "step_2",
- "data": {
- "table_name": "订单表",
- "watched_field_name": "订单编号"
- }
- },
- {
- "id": "step_2",
- "type": "IfElseBranch",
- "title": "判断订单金额是否大于 1000",
- "children": {
- "links": [
- { "kind": "if_true", "to": "step_3" },
- { "kind": "if_false", "to": "step_4" }
- ]
- },
- "next": "step_5",
- "data": {
- "condition": {
- "conjunction": "or",
- "conditions": [{
- "conjunction": "and",
- "conditions": [{
- "left_value": { "value_type": "ref", "value": "$.step_1.fieldxxx" },
- "operator": "isGreater",
- "right_value": [{ "value_type": "number", "value": 1000 }]
- }]
- }]
- }
- }
- },
- {
- "id": "step_3",
- "type": "LarkMessageAction",
- "title": "通知主管审批大额订单",
- "next": null,
- "data": {
- "receiver": [{ "value_type": "ref", "value": "$.step_1.fieldxxx" }],
- "send_to_everyone": false,
- "title": [{ "value_type": "text", "value": "大额订单提醒" }],
- "content": [
- { "value_type": "text", "value": "新订单金额为:" },
- { "value_type": "ref", "value": "$.step_1.fieldxxx" },
- { "value_type": "text", "value": "元,请及时审批。" }
- ],
- "btn_list": []
- }
- },
- {
- "id": "step_4",
- "type": "SetRecordAction",
- "title": "自动标记小额订单为已通过",
- "next": null,
- "data": {
- "table_name": "订单表",
- "ref_info": { "step_id": "step_1" },
- "field_values": [
- { "field_name": "审批状态", "value": [{ "value_type": "text", "value": "已通过" }] }
- ]
- }
- },
- {
- "id": "step_5",
- "type": "GenerateAiTextAction",
- "title": "AI 生成订单处理日报",
- "next": null,
- "data": {
- "prompt": [
- { "value_type": "text", "value": "请根据以下订单信息生成一份简要的处理日报:" },
- { "value_type": "ref", "value": "$.step_1.fieldxxx" }
- ]
- }
- }
- ]
-}
-```
-
----
+只有在需要构造 `value_type`、`ref`、条件过滤、字段值、节点输出引用时,才读 [common-types-and-refs.md](workflow-steps/common-types-and-refs.md)。
## 参考
-- [lark-base-workflow-guide.md](lark-base-workflow-guide.md) — 完整示例和构造技巧
-- 创建/更新时外层只承载 workflow 元信息,核心校验对象是 `steps`;列表只用于拿 workflow ID 和启停状态
+- Workflow 创建/更新入口路由:[lark-base-workflow-guide.md](lark-base-workflow-guide.md)
+- 命令参数以 `lark-cli base +workflow-create --help` / `+workflow-update --help` 为准;只有参数不确定或命令报错时才读取 help。
diff --git a/skills/lark-base/references/workflow-steps/action-add-record.md b/skills/lark-base/references/workflow-steps/action-add-record.md
new file mode 100644
index 00000000..95eb7035
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/action-add-record.md
@@ -0,0 +1,24 @@
+# AddRecordAction
+
+```json
+{
+ "table_name": "订单表",
+ "field_values": [
+ { "field_name": "客户名称", "value": [{ "value_type": "text", "value": "张三" }] },
+ { "field_name": "金额", "value": [{ "value_type": "number", "value": 100 }] },
+ { "field_name": "创建人", "value": [{ "value_type": "ref", "value": "$.trigger_1.fieldIdxxx" }] }
+ ]
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `table_name` | 是 | 目标数据表名 |
+| `field_values` | 是 | RecordFieldValue[] |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/action-delay.md b/skills/lark-base/references/workflow-steps/action-delay.md
new file mode 100644
index 00000000..4e357786
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/action-delay.md
@@ -0,0 +1,16 @@
+# Delay
+
+```json
+{ "duration": 30 }
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `duration` | 是 | 延迟时长(分钟),范围 [1, 120] |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/action-find-record.md b/skills/lark-base/references/workflow-steps/action-find-record.md
new file mode 100644
index 00000000..ecaaed8d
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/action-find-record.md
@@ -0,0 +1,25 @@
+# FindRecordAction
+
+```json
+{
+ "table_name": "客户表",
+ "field_names": ["客户名称", "联系方式", "等级"],
+ "should_proceed_when_no_results": true,
+ "filter_info": { /* RecordFilterInfo */ }
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `table_name` | 是 | 目标数据表名 |
+| `field_names` | 是 | 要检索的字段名列表,至少一个 |
+| `should_proceed_when_no_results` | 否 | 无结果时是否继续后续步骤,默认 `true` |
+| `filter_info` | 否* | RecordFilterInfo(与 `ref_info` 互斥) |
+| `ref_info` | 否* | RefInfo(与 `filter_info` 互斥) |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/action-generate-ai-text.md b/skills/lark-base/references/workflow-steps/action-generate-ai-text.md
new file mode 100644
index 00000000..556243b4
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/action-generate-ai-text.md
@@ -0,0 +1,21 @@
+# GenerateAiTextAction
+
+```json
+{
+ "prompt": [
+ { "value_type": "text", "value": "请总结以下内容:" },
+ { "value_type": "ref", "value": "$.step_1.fieldxxx" }
+ ]
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `prompt` | 是 | TextRefItem[] 提示词,支持 `text` / `ref` |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/action-http-client.md b/skills/lark-base/references/workflow-steps/action-http-client.md
new file mode 100644
index 00000000..a7a4abc0
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/action-http-client.md
@@ -0,0 +1,48 @@
+# HTTPClientAction
+
+```json
+{
+ "method": "POST",
+ "url": [{ "value_type": "text", "value": "https://api.example.com/webhook" }],
+ "queries": [
+ { "key": "source", "value": [{ "value_type": "text", "value": "workflow" }] }
+ ],
+ "headers": [
+ { "key": "Content-Type", "value": [{ "value_type": "text", "value": "application/json" }] }
+ ],
+ "body_type": "raw",
+ "raw_body": [
+ { "value_type": "text", "value": "{\"record_id\":\"" },
+ { "value_type": "ref", "value": "$.step_1.recordId" },
+ { "value_type": "text", "value": "\"}" }
+ ],
+ "response_type": "json",
+ "response_value": "{\"success\":true,\"message\":\"data fetched successfully\"}"
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|-----|------|
+| `method` | 否 | 请求方法:`GET` / `POST` / `PUT` / `PATCH` / `DELETE`,默认 `POST` |
+| `url` | 是 | ValueInfo[],请求 URL,支持 `text` / `ref` 拼接 |
+| `queries` | 否 | KeyValue[],查询参数 |
+| `headers` | 否 | KeyValue[],请求头 |
+| `body_type` | 否 | 请求体类型:`none` / `raw` / `form-data` / `form-urlencoded`,默认 `raw` |
+| `raw_body` | 否 | ValueInfo[],原始请求体,仅 `body_type=raw` 时使用 |
+| `form_body` | 否 | KeyValue[],表单数据,仅 `body_type=form-data` 或 `body_type=form-urlencoded` 时使用 |
+| `response_type` | 否 | 响应类型:`none` / `text` / `json`,默认 `json` |
+| `response_value` | 否 | string,JSON 字符串形式的响应结果示例;仅当 `response_type=json` 时必填 |
+
+`KeyValue`:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `key` | string | 参数名 / 请求头名 |
+| `value` | ValueInfo[] | 参数值 / 请求头值,支持 `text` / `ref` |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/action-lark-message.md b/skills/lark-base/references/workflow-steps/action-lark-message.md
new file mode 100644
index 00000000..13e8357a
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/action-lark-message.md
@@ -0,0 +1,42 @@
+# LarkMessageAction
+
+```json
+{
+ "receiver": [{ "value_type": "user", "value": {"id": "ou_xxxx"} }],
+ "send_to_everyone": false,
+ "title": [{ "value_type": "text", "value": "新订单通知" }],
+ "content": [
+ { "value_type": "text", "value": "客户 " },
+ { "value_type": "ref", "value": "$.trigger_1.fldCustomerName" },
+ { "value_type": "text", "value": " 创建了新订单" }
+ ],
+ "btn_list": [
+ { "text": "查看详情", "btn_action": "openLink", "link": [{ "value_type": "text", "value": "https://example.com" }] }
+ ]
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `receiver` | 是 | ValueInfo[] |
+| `send_to_everyone` | 是 | 是否发送给所有人 |
+| `title` | 否 | TextRefItem[] 消息标题 |
+| `content` | 是 | TextRefItem[] 消息内容 |
+| `btn_list` | 是 | 按钮列表,不需要时为空数组 |
+
+`ButtonConfig`:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `text` | string | 按钮文字 |
+| `btn_action` | string | `addRecord` / `setRecord` / `openLink` |
+| `link` | ValueInfo[] | 跳转链接(`openLink` 时使用) |
+| `table_name` | string | 操作表名(`addRecord` 时使用) |
+| `record_values` | RecordFieldValue[] | 记录赋值(`addRecord` / `setRecord` 时使用) |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/action-set-record.md b/skills/lark-base/references/workflow-steps/action-set-record.md
new file mode 100644
index 00000000..eeab3173
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/action-set-record.md
@@ -0,0 +1,28 @@
+# SetRecordAction
+
+```json
+{
+ "table_name": "订单表",
+ "max_set_record_num": 10,
+ "field_values": [
+ { "field_name": "状态", "value": [{ "value_type": "option", "value": { "id": "opt1", "name": "已完成" } }] }
+ ],
+ "filter_info": { /* RecordFilterInfo */ },
+ "ref_info": { "step_id": "step_trigger" }
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `table_name` | 是 | 目标数据表名 |
+| `max_set_record_num` | 否 | 最大更新记录数,默认 100,范围 1-15000 |
+| `field_values` | 是 | RecordFieldValue[] |
+| `filter_info` | 否* | RecordFilterInfo 过滤条件(与 `ref_info` 互斥) |
+| `ref_info` | 否* | RefInfo 引用前置步骤的记录(与 `filter_info` 互斥) |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/branch-if-else.md b/skills/lark-base/references/workflow-steps/branch-if-else.md
new file mode 100644
index 00000000..31bd4389
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/branch-if-else.md
@@ -0,0 +1,36 @@
+# IfElseBranch
+
+`children.links` 包含 `if_true` 和 `if_false` 两条边,`next` 指向两个分支汇合后的后继节点。
+
+**如果涉及到复杂的多分支场景(分支数目 >= 3时),你应该采用 SwitchBranch,而不是嵌套的 IfElseBranch**
+
+```json
+{
+ "condition": {
+ "conjunction": "or",
+ "conditions": [
+ {
+ "conjunction": "and",
+ "conditions": [
+ {
+ "left_value": { "value_type": "ref", "value": "$.step_1.fieldxxx" },
+ "operator": "isGreater",
+ "right_value": [{ "value_type": "number", "value": 1000 }]
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `condition` | 是 | OrGroup 判断条件,结构为 `(A and B) or (C and D)` |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/branch-switch.md b/skills/lark-base/references/workflow-steps/branch-switch.md
new file mode 100644
index 00000000..eda55ad7
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/branch-switch.md
@@ -0,0 +1,52 @@
+# SwitchBranch
+
+`children.links` 包含多个 `case` 边(`label` 建议用 `branch_1`、`branch_2`,语义写在 `desc`)。
+
+```json
+{
+ "mode": "exclusive",
+ "no_match_action": "classifyToOther",
+ "child_branch_list": [
+ {
+ "name": "高优先级",
+ "condition": {
+ "conjunction": "or",
+ "conditions": [
+ {
+ "conjunction": "and",
+ "conditions": [
+ {
+ "left_value": { "value_type": "ref", "value": "$.step_1.fieldxxx" },
+ "operator": "is",
+ "right_value": [{ "value_type": "text", "value": "P0" }]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `mode` | 否 | 分支模式。`exclusive`:排他模式,仅执行一个满足条件的子分支;`parallel`:并行模式,执行所有满足条件的子分支。默认 `exclusive` |
+| `no_match_action` | 否 | `mode=exclusive` 时使用,无匹配时的处理策略。`classifyToOther`:归类到其他分支;`fail`:报错终止。默认 `classifyToOther` |
+| `fail_mode` | 否 | `mode=parallel` 时使用,部分分支出错时策略。`partialSuccess`:部分成功即继续;`fail`:任一失败即终止。默认 `partialSuccess` |
+| `match_mode` | 否 | `mode=parallel` 时使用,所有分支不满足时策略。`noneMatchSkip`:跳过继续;`noneMatchFail`:报错终止。默认 `noneMatchSkip` |
+| `child_branch_list` | 是 | BranchItem[],1-10 个条件分支 |
+
+`BranchItem`:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `name` | string | 分支名称 |
+| `condition` | OrGroup | 分支条件 |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/common-types-and-refs.md b/skills/lark-base/references/workflow-steps/common-types-and-refs.md
new file mode 100644
index 00000000..5b038bd1
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/common-types-and-refs.md
@@ -0,0 +1,401 @@
+# Workflow common types and refs
+
+### ValueInfo
+
+所有值的基础类型,通过 `value_type` 区分:
+
+| value_type | value 类型 | 说明 | 示例 |
+|------------|-----------|------|------|
+| `text` | string | 文本 | `"张三"` |
+| `number` | number | 数字 | `100` |
+| `boolean` | boolean | 布尔值 | `true` |
+| `date` | string | 日期,可以是具体时间字符串,或者相对时间值 | `"2025/01/01"`、`"2025/01/01 11:00"`、`"now"`、`"now 11:00"`、`"today"`、`"today 11:00"`、`"yesterday"`、`"yesterday 11:00"`、`"lastWeek"`、`"currentMonth"`、`"lastMonth"`、`"theLastWeek"`、`"theNextWeek"`、`"theLastMonth"`、`"theNextMonth"` |
+| `option` | `{ id, name }` | 选项 | `{ "id": "opt1", "name": "已完成" }` |
+| `link` | `{ text, link }` | 链接(含文字和 URL), 文字和 URL 的格式可以是 ValueInfo 中的 text/ref 类型 | `{ "text": [{ "value_type": "text", "value": "查看详情" }], "link": [{ "value_type": "text", "value": "https://example.com" }] }`、`{ "text": [{ "value_type": "text", "value": "查看详情" }], "link": [{ "value_type": "ref", "value": "$.step_1.fldXXX" }] }` |
+| `user` | `{ id, name }` | 用户 OpenID、名字 | `{ "id": "ou_xxxx", "name": "张三" }` |
+| `group` | `{ id, name }` | 群 Chat ID、名字 | `{ "id": "oc_xxx", "name": "测试群" }` |
+| `ref` | `string` | 引用前置节点输出的路径 | 参考 ref 引用变量详解 章节 |
+
+> ⚠️ **所有涉及用户的 value 中的 id 统一使用 OpenID(`ou_xxxx` 格式)**,由 CLI 层来完成转换
+> ⚠️ **所有涉及群的 value 中的 id 统一使用 ChatID(`oc_xxxx` 格式)**,由 CLI 层来完成转换
+
+### ref 引用变量详解
+
+`ref` 类型是工作流中节点间数据传递的核心机制。当 `value_type` 为 `ref` 时,`value` 指向前置节点的某个输出变量。本节详细描述每个节点可供引用的输出变量定义。
+
+#### 引用路径格式
+
+```
+$.{stepId}
+$.{stepId}.{pathId}
+$.{stepId}.{pathId}.{childPathId}
+$.{stepId}.{pathId}.{childPathId}.{grandChildPathId}
+```
+
+- `{stepId}`:前置节点的 `id`(即 WorkflowStep 中的 `id` 字段)
+- `{pathId}`:节点输出的路径标识符
+- 支持多层下钻,如引用字段的属性:`$.step_1.fldXXX.name`
+
+---
+
+#### 触发器节点输出
+
+##### 记录触发器(AddRecordTrigger / ChangeRecordTrigger / SetRecordTrigger / ReminderTrigger)
+
+这 4 个触发器的输出结构完全一致:
+
+| pathId | 说明 | 引用示例 |
+|--------|------|----------|
+| `{fieldId}` | 字段id,从配置表的所有字段或者指定字段id生成,可下钻字段属性 | `$.{stepId}.{fieldId}` |
+| `{fieldId}.fieldId` | 字段id属性 | `$.{stepId}.{fieldId}.fieldId` |
+| `{fieldId}.fieldName` | 字段名属性 | `$.{stepId}.{fieldId}.fieldName` |
+| `startTime` | 触发时间戳 | `$.{stepId}.startTime` |
+| `recordId` | 记录 ID | `$.{stepId}.recordId` |
+| `recordLink` | 记录链接 | `$.{stepId}.recordLink` |
+| `recordCreatedUser` | 记录创建者 | `$.{stepId}.recordCreatedUser` |
+| `recordCreatedTime` | 记录创建时间 | `$.{stepId}.recordCreatedTime` |
+| `recordModifiedUser` | 最后修改者 | `$.{stepId}.recordModifiedUser` |
+| `recordModifiedTime` | 最后修改时间 | `$.{stepId}.recordModifiedTime` |
+
+**动态字段输出规则**:
+
+- 读取触发器所配置的数据表的所有字段
+- 每个字段生成一条输出:`pathId` = fieldId
+- 若字段为关联字段,children 为关联表所有字段(单层下钻,不再递归)
+- 每个字段可下钻特定的字段属性(见「字段属性下钻」)
+
+**recordLink 的 children**:如果配置了数据表,则为该表所有视图的列表,每个视图 `{ pathId: viewId, pathName: viewName, pathType: 'string' }`。引用示例:`$.{stepId}.recordLink.{viewId}`。
+
+##### ButtonTrigger(按钮触发器)
+
+`ButtonTrigger` 的输出取决于 `button_type`:
+
+#### `button_type = buttonField`
+
+| pathId | 说明 | 引用示例 |
+|--------|------|----------|
+| `{fieldId}` | 字段id,从配置表的所有字段或者指定字段id生成,可下钻字段属性 | `$.{stepId}.{fieldId}` |
+| `{fieldId}.fieldId` | 字段id属性 | `$.{stepId}.{fieldId}.fieldId` |
+| `{fieldId}.fieldName` | 字段名属性 | `$.{stepId}.{fieldId}.fieldName` |
+| `recordId` | 记录 ID | `$.{stepId}.recordId` |
+| `recordLink` | 记录链接 | `$.{stepId}.recordLink` |
+| `recordCreatedUser` | 记录创建者 | `$.{stepId}.recordCreatedUser` |
+| `recordModifiedUser` | 最后修改者 | `$.{stepId}.recordModifiedUser` |
+| `recordModifiedTime` | 最后修改时间 | `$.{stepId}.recordModifiedTime` |
+| `time` | 触发时间 | `$.{stepId}.time` |
+| `user` | 触发人 | `$.{stepId}.user` |
+| `buttonName` | 触发的按钮名称 | `$.{stepId}.buttonName` |
+
+#### `button_type = buttonElement`
+
+| pathId | 说明 | 引用示例 |
+|--------|------|----------|
+| `time` | 触发时间 | `$.{stepId}.time` |
+| `user` | 触发人 | `$.{stepId}.user` |
+| `buttonName` | 触发的按钮名称 | `$.{stepId}.buttonName` |
+
+##### TimerTrigger(定时触发器)
+
+| pathId | 说明 | 引用示例 |
+|--------|------|----------|
+| `scheduleTime` | 定时触发时间 | `$.{stepId}.scheduleTime` |
+
+##### LarkMessageTrigger(飞书消息触发器)
+
+| pathId | 说明 | 引用示例 |
+|--------|------|----------|
+| `Sender` | 消息发送者 | `$.{stepId}.Sender` |
+| `AtUser` | 消息中被@的用户 | `$.{stepId}.AtUser` |
+| `SenderGroup` | 消息所在群(仅群聊场景) | `$.{stepId}.SenderGroup` |
+| `MessageSendTime` | 消息发送时间 | `$.{stepId}.MessageSendTime` |
+| `MessageContent` | 消息正文 | `$.{stepId}.MessageContent` |
+| `MessageType` | 消息类型标识 | `$.{stepId}.MessageType` |
+| `MessageID` | 消息唯一标识 | `$.{stepId}.MessageID` |
+| `MessageLink` | 消息链接(仅群聊场景) | `$.{stepId}.MessageLink` |
+| `ParentID` | 回复的消息 ID | `$.{stepId}.ParentID` |
+| `ThreadID` | 所在话题消息 ID | `$.{stepId}.ThreadID` |
+| `Attachments` | 消息中的附件 | `$.{stepId}.Attachments` |
+
+条件限制:
+
+- 若场景为单聊(`receive_scene = "Chat"`),则 `SenderGroup` 和 `MessageLink` 不可用
+
+---
+
+#### 操作节点输出
+
+##### FindRecordAction(查找记录)
+
+| pathId | 说明 | 引用示例|
+|--------|------|-------|
+| `fieldRecords` | 所有找到的记录的引用(可用于 Loop 遍历) | `$.{stepId}.fieldRecords`|
+| `firstfieldsRecord` | 第一条匹配记录 | `$.{stepId}.firstfieldsRecord`|
+| `firstfieldsRecord.{fieldId}` | 首条记录的字段值,可下钻字段属性 | `$.{stepId}.firstfieldsRecord.{fieldId}`|
+| `firstfieldsRecord.recordId` | 记录 ID 数组 | `$.{stepId}.firstfieldsRecord.recordId`|
+| `fields` | 查找到的所有记录某列值 | 不支持引用|
+| `fields.{fieldId}` | 用户选择的字段 | `$.{stepId}.fields.{fieldId}`|
+| `fields.{fieldId}.fieldId` | 用户选择的字段id数组 | `$.{stepId}.fields.{fieldId}.fieldId`|
+| `fields.{fieldId}.fieldName` | 用户选择的字段名数组 | `$.{stepId}.fields.{fieldId}.fieldName`|
+| `fields.recordId` | 记录 ID 数组 | `$.{stepId}.fields.recordId`|
+| `recordNum` | 找到记录总数 | `$.{stepId}.recordNum`|
+
+##### AddRecordAction(新增记录)
+
+| pathId | 说明 | 引用示例 |
+|--------|------|----------|
+| `{fieldId}` | 用户配置的字段值,可下钻字段属性 | `$.{stepId}.{fieldId}` |
+| `{fieldId}.fieldId` | 用户配置的字段id | `$.{stepId}.{fieldId}.fieldId` |
+| `{fieldId}.fieldName` | 用户配置的字段名 | `$.{stepId}.{fieldId}.fieldName` |
+| `recordId` | 新增的记录 ID | `$.{stepId}.recordId` |
+| `recordLink` | 新增的记录 URL | `$.{stepId}.recordLink` |
+
+##### SetRecordAction(更新记录)
+
+| pathId | 说明 | 引用示例 |
+|--------|------|----------|
+| `{fieldId}` | 用户配置的字段值,可下钻字段属性 | `$.{stepId}.{fieldId}` |
+| `{fieldId}.fieldId` | 用户配置的字段id | `$.{stepId}.{fieldId}.fieldId` |
+| `{fieldId}.fieldName` | 用户配置的字段名 | `$.{stepId}.{fieldId}.fieldName` |
+| `recordId` | 记录 ID 数组(因可能更新多条记录) | `$.{stepId}.recordId` |
+
+##### HTTPClientAction(HTTP 请求)
+
+HTTPClientAction 的输出取决于 `response_type`:
+
+| response_type | 是否可引用 | 输出说明 | 引用示例 |
+|--------------|-----------|----------|----------|
+| `none` | 否 | 无任何可引用输出 | 不支持引用 |
+| `text` | 是 | 整个响应文本作为节点整体输出 | `$.{stepId}` |
+| `json` | 是 | 响应体整体挂在 `body` 下,同时返回 `status_code`;仅可引用 `response_value` 中声明的字段 | `$.{stepId}.body`、`$.{stepId}.body.success`、`$.{stepId}.body.message`、`$.{stepId}.status_code` |
+
+**补充说明**:
+
+- 当 `response_type = none` 时,后续节点无法引用 HTTPClientAction 的任何输出
+- 当 `response_type = text` 时,`$.{stepId}` 表示整个响应文本
+- 当 `response_type = json` 时,`$.{stepId}.body` 表示整个 JSON body,`$.{stepId}.body.字段名` 表示 body 中某个字段
+- 仅当 `response_type = json` 时,`$.{stepId}.status_code` 表示请求该 HTTP URL 后返回的 HTTP 状态码
+- 仅当 `response_type = json` 时,`response_value` 必填
+- 当 `response_type = json` 时,后续节点只能引用 `response_value` 中声明过的字段
+
+**案例**:
+
+假设某个 `HTTPClientAction` 的配置如下:
+
+```json
+{
+ "id": "step_http_1",
+ "type": "HTTPClientAction",
+ "data": {
+ "response_type": "json",
+ "response_value": "{\"success\":true,\"message\":\"ok\"}"
+ }
+}
+```
+
+则后续节点仅可以引用:
+
+- `$.step_http_1.body`
+- `$.step_http_1.body.success`
+- `$.step_http_1.body.message`
+- `$.step_http_1.status_code`
+
+但**不能**引用未在 `response_value` 中声明的字段,例如:
+
+- `$.step_http_1.body.data`
+- `$.step_http_1.body.request_id`
+
+##### GenerateAiTextAction(AI 生成文本)
+
+| pathId | 说明 | 引用示例 |
+|--------|------|----------|
+| (整体出参) | AI 生成的文本内容(不支持下钻,只能引用 `$.{stepId}`) | `$.{stepId}` |
+
+##### 无输出的操作节点
+
+以下节点不产生任何可引用的输出数据:
+
+- **Delay**(延时等待)
+- **LarkMessageAction**(发送飞书消息)
+
+---
+
+#### 分支节点输出
+
+以下分支节点均不产生任何可引用的输出数据:
+
+- **IfElseBranch**(条件分支)
+- **SwitchBranch**(多条件分支)
+
+---
+
+#### 系统节点输出
+
+##### Loop(循环)
+
+| pathId | 说明 | 引用示例 |
+|--------|------|----------|
+| `item` | 当前循环元素 | `$.{stepId}.item` |
+| `index` | 从 0 开始的循环索引 | `$.{stepId}.index` |
+
+**`item` 的类型推断规则**(由循环数据源决定):
+
+**场景一:遍历组合记录** — 数据源为 `record` 类型时(如 FindRecordAction 的 `fieldRecords`),`item` 类型为 `record`,可向下选择具体字段:
+
+| 说明 | 引用示例 |
+|------|----------|
+| 当前遍历的记录(record) | `$.{loopStepId}.item` |
+| 记录的具体字段 | `$.{loopStepId}.item.{fieldId}` |
+| 从 0 开始的索引(number) | `$.{loopStepId}.index` |
+
+**场景二:遍历字段** — 数据源为某个多值类型字段时,比如附件字段、人员字段,`item` 继承该字段的类型并可继续下钻字段属性:
+
+| 说明 | 引用示例 |
+|------|----------|
+| 当前遍历的元素(类型继承数据源字段类型,例如人员字段) | `$.{loopStepId}.item` |
+| 用户姓名 | `$.{loopStepId}.item.name` |
+| 从 0 开始的索引(number) | `$.{loopStepId}.index` |
+
+---
+
+#### 字段属性下钻
+
+每个字段变量都可以进一步下钻选择字段的属性。所有字段至少支持 `fieldId` 和 `fieldName` 两个基础属性,部分字段还支持额外属性:
+
+| 字段类型 | 属性名称 | 属性 pathId | 属性 pathType | 说明 |
+|----------|---------|-------------|--------------|------|
+| **所有字段(基础)** | 字段 ID | `fieldId` | `string` | 字段的唯一标识 |
+| | 字段名称 | `fieldName` | `string` | 字段的显示名称 |
+| **人员字段**(`user` / `created_by` / `updated_by`) | 姓名 | `name` | `string` | 用户姓名 |
+| **日期字段**(`datetime` / `created_at` / `updated_at`) | 时间戳 | `timestamp` | `number` | 时间戳数值 |
+| **附件字段**(`attachment`) | 文件名 | `fileName` | `string` | 附件文件名 |
+| | 文件类型 | `fileType` | `string` | MIME 类型 |
+| | 文件大小 | `size` | `number` | 文件字节数 |
+| | 文件 Token | `fileToken` | `string` | 附件 token |
+| **超链接文本字段**(`text` 且 `style.type=url`) | 文本 | `text` | `string` | 链接文本部分 |
+| | 链接 | `link` | `string` | 链接 URL 部分 |
+| **自动编号字段**(`auto_number`) | 序号 | `sequence` | `number` | 编号的纯数字序号 |
+| **关联字段**(`link`) | 字段下钻 | `{fieldId}` | - | 可下钻到关联表的字段 |
+
+> 其他字段类型(如 `text`、`number`、`checkbox`、`select`、`location`、`formula`、`lookup` 等)仅支持 `fieldId` 和 `fieldName` 两个基础属性。
+
+下钻引用示例:
+
+```
+$.{stepId}.{fieldId} → 字段值本身
+$.{stepId}.{fieldId}.fieldId → 字段 ID(string)
+$.{stepId}.{fieldId}.fieldName → 字段名称(string)
+$.{stepId}.{fieldId}.name → 人员姓名列表(array,仅人员字段)
+$.{stepId}.{fieldId}.unionId → 人员 unionId 列表(array,仅人员字段)
+$.{stepId}.{fieldId}.timestamp → 时间戳(array,仅日期字段)
+$.{stepId}.{fieldId}.fileName → 文件名列表(array,仅附件字段)
+$.{stepId}.{fieldId}.fileToken → 文件 Token 列表(array,仅附件字段)
+```
+
+---
+
+#### 节点输出能力总览
+
+| 节点 | 类型 | 有输出 | 输出特性 |
+|------|------|--------|---------|
+| AddRecordTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性) |
+| ChangeRecordTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性) |
+| SetRecordTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性) |
+| ReminderTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性) |
+| ButtonTrigger | 触发器 | ✅ | 动态(表字段 + 记录属性;buttonElement 仅基础触发属性) |
+| TimerTrigger | 触发器 | ✅ | 静态(仅 scheduleTime) |
+| LarkMessageTrigger | 触发器 | ✅ | 静态(消息属性列表) |
+| FindRecordAction | 动作 | ✅ | 动态(用户选择的字段) |
+| AddRecordAction | 动作 | ✅ | 动态(用户配置的字段) |
+| SetRecordAction | 动作 | ✅ | 动态(用户配置的字段) |
+| HTTPClientAction | 动作 | ✅ | 动态(取决于用户配置的 HTTP 响应输出) |
+| GenerateAiTextAction | 动作 | ✅ | 静态(单 string) |
+| Delay | 动作 | ❌ | 无输出 |
+| LarkMessageAction | 动作 | ❌ | 无输出 |
+| IfElseBranch | 分支 | ❌ | 无输出 |
+| SwitchBranch | 分支 | ❌ | 无输出 |
+| Loop | 系统 | ✅ | 动态(取决于数据源) |
+
+---
+
+### TextRefItem
+
+文本与引用混排,用于消息内容等动态拼接场景:
+
+```json
+[
+ { "value_type": "text", "value": "客户 " },
+ { "value_type": "ref", "value": "$.step_1.fieldxxx" },
+ { "value_type": "text", "value": " 创建了新订单" }
+]
+```
+
+### RecordFieldValue
+
+```json
+{ "field_name": "客户名称", "value": [{ "value_type": "text", "value": "张三" }] }
+```
+
+### AndCondition(Trigger 过滤条件)
+
+```json
+{
+ "conjunction": "and",
+ "conditions": [
+ { "field_name": "状态", "operator": "is", "value": [{ "value_type": "text", "value": "进行中" }] }
+ ]
+}
+```
+
+### OrGroup(Branch 分支条件)
+
+```json
+{
+ "conjunction": "or",
+ "conditions": [
+ {
+ "conjunction": "and",
+ "conditions": [
+ {
+ "left_value": { "value_type": "ref", "value": "$.step_1.fieldxxx" },
+ "operator": "isGreater",
+ "right_value": [{ "value_type": "number", "value": 1000 }]
+ }
+ ]
+ }
+ ]
+}
+```
+
+**operator 可选值:** `is` / `isNot` / `containsAny` / `doesNotContainAny` / /`containsAll`/ `isEmpty` / `isNotEmpty` / `isGreater` / `isGreaterEqual` / `isLess` / `isLessEqual`
+
+### RecordFilterInfo
+** 由于 conjunction 只支持 and,若需要实现 字段X 等于 A 或 B,你可以使用 containsAny
+```json
+{
+ "conjunction": "and",
+ "conditions": [
+ { "field_name": "状态", "operator": "is", "value": [{ "value_type": "text", "value": "进行中" }] }
+ ]
+}
+```
+
+### `select` 字段多值匹配
+
+| 操作 | operator | 正确写法 |
+|------|---------|---------|
+| 等于单个值 | `is` | `[{"value_type": "option", "value": {"name": "L2"}}]` |
+| 匹配多个值(L2 或 L3) | `containsAny` | `[{"value_type": "option", "value": {"name": "L2"}}, {"value_type": "option", "value": {"name": "L3"}}]` |
+
+> ⚠️ 不要用多个 `is` 条件(会被当作 OR,无法实现 AND)。推荐使用 `containsAny` 操作符匹配多个值。
+
+> ⚠️ **Select 字段条件**:`value_type` 必须为 `option`,`value` 对象可只传 `name`(如 `{"name": "L2"}`),无需提供选项 ID。
+
+### RefInfo
+
+```json
+{ "step_id": "step_trigger" }
+```
+
+---
+
+返回 [Workflow schema index](../lark-base-workflow-schema.md)。
diff --git a/skills/lark-base/references/workflow-steps/system-loop.md b/skills/lark-base/references/workflow-steps/system-loop.md
new file mode 100644
index 00000000..93ee7544
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/system-loop.md
@@ -0,0 +1,26 @@
+# Loop
+
+`children.links` 包含 `loop_start` 边指向循环体入口,`next` 指向循环结束后的后继节点。
+
+```json
+{
+ "loop_mode": "continue",
+ "max_loop_times": 100,
+ "data": [{ "value_type": "ref", "value": "$.find_record_stepIdxxx.fieldRecords" }]
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `data` | 是 | ValueInfo[](仅支持 `ref` 类型),循环数据源,只能填一个 |
+| `loop_mode` | 否 | 单次错误时是否继续:`end`(终止)/ `continue`(继续) |
+| `max_loop_times` | 否 | 最大循环次数 |
+
+---
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/trigger-add-record.md b/skills/lark-base/references/workflow-steps/trigger-add-record.md
new file mode 100644
index 00000000..272e4b6c
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/trigger-add-record.md
@@ -0,0 +1,24 @@
+# AddRecordTrigger
+
+```json
+{
+ "table_name": "订单表",
+ "watched_field_name": "状态",
+ "trigger_control_list": ["pasteUpdate", "automationBatchUpdate"],
+ "condition_list": [] /* AndCondition 数组 */
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `table_name` | 是 | 监控的数据表名 |
+| `watched_field_name` | 是 | 监控的字段名 |
+| `trigger_control_list` | 否 | 触发控制,可选值:`pasteUpdate` / `automationBatchUpdate` / `syncUpdate` / `appendImport` / `openAPIBatchUpdate` |
+| `condition_list` | 否 | 过滤条件数组,数组中每个元素为 AndCondition 结构,多个 AndCondition 之间为 OR 关系 |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/trigger-button.md b/skills/lark-base/references/workflow-steps/trigger-button.md
new file mode 100644
index 00000000..f54f0d1a
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/trigger-button.md
@@ -0,0 +1,22 @@
+# ButtonTrigger
+
+```json
+{
+ "button_type": "buttonField",
+ "table_name": "审批表"
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `button_type` | 是 | 按钮类型:`buttonField`(表格里的按钮,可操作当前记录数据)/ `buttonElement`(仪表盘、应用页面上的按钮,可执行整体操作) |
+| `table_name` | 否 | 绑定的数据表名,仅 `button_type=buttonField` 时填写 |
+
+> `buttonField` 和 `buttonElement` 的输出能力不同,详见下方「ButtonTrigger(按钮触发器)」输出说明。
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/trigger-change-record.md b/skills/lark-base/references/workflow-steps/trigger-change-record.md
new file mode 100644
index 00000000..4ca0847b
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/trigger-change-record.md
@@ -0,0 +1,24 @@
+# ChangeRecordTrigger
+
+记录满足条件时触发,**新增和修改都会触发**。"修改为 X 或新增 X 时执行动作"这类需求用本触发器 + `condition_list`,一条工作流即可表达,不要拆成 AddRecordTrigger 和 SetRecordTrigger 两条。
+
+```json
+{
+ "table_name": "任务表",
+ "trigger_control_list": [],
+ "condition": null
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `table_name` | 是 | 监控的数据表名 |
+| `trigger_control_list` | 否 | 触发控制,可选值:`pasteUpdate` / `automationBatchUpdate` / `syncUpdate` / `appendImport` |
+| `condition_list` | 否 | 过滤条件数组,数组中每个元素为 AndCondition 结构,多个 AndCondition 之间为 OR 关系 |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/trigger-lark-message.md b/skills/lark-base/references/workflow-steps/trigger-lark-message.md
new file mode 100644
index 00000000..30d7a906
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/trigger-lark-message.md
@@ -0,0 +1,40 @@
+# LarkMessageTrigger
+
+```json
+{
+ "receive_scene": "group",
+ "receiver": [{ "value_type": "group", "value": {"id": "oc_xxxx", "name": "测试群"} }],
+ "scope": "all",
+ "filter": {
+ "conjunction": "and",
+ "content_contains": ["关键词"],
+ "sender_contains": [{ "value_type": "user", "value": {"id": "ou_xxxx", "name": ""} }],
+ "is_new_message": true,
+ "is_message_contain_attachment": false
+ }
+}
+```
+
+| 字段 | 必填 | 说明|
+|------|------|---|
+| `receive_scene` | 是 | 接收场景:`group`(群聊)/ `chat`(单聊)|
+| `receiver` | 是 | 触发来源,支持 `user` / `group` / `ref`。在单聊场景下,该字段指“可以和机器人单聊的用户”;在群聊场景下,该字段指“接收信息的群组”|
+| `scope` | 是 | 触发范围:`at`(@提及)/ `all`(所有消息)。该参数仅在群聊场景有效,单聊场景请勿指定该参数|
+| `filter` | 是 | MessageFilter 消息过滤条件|
+
+`MessageFilter`:
+
+| 字段 | 类型 | 说明 |
+|------|------|----|
+| `conjunction` | string | `and` 满足所有条件 / `or` 任一条件|
+| `content_contains` | string[] | 关键词列表|
+| `sender_contains` | ValueInfo[] | 筛选发送人(仅群聊+群组来源时生效,单聊场景请勿指定该参数)|
+| `is_new_message` | boolean | 仅新话题消息(仅群聊时有效,单聊场景请勿指定该参数)|
+| `is_message_contain_attachment` | boolean | 是否仅附件消息触发|
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/trigger-reminder.md b/skills/lark-base/references/workflow-steps/trigger-reminder.md
new file mode 100644
index 00000000..96fa4324
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/trigger-reminder.md
@@ -0,0 +1,30 @@
+# ReminderTrigger
+
+```json
+{
+ "table_name": "项目表",
+ "field_name": "截止日期",
+ "offset": 1,
+ "unit": "DAY",
+ "hour": 9,
+ "minute": 0,
+ "condition_list": null
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `table_name` | 是 | 数据表名 |
+| `field_name` | 是 | 日期字段名(必须为 `datetime` / `created_at` / `formula` / `lookup` 类型) |
+| `unit` | 是 | 偏移单位:`MINUTE` / `HOUR` / `DAY` / `WEEK` / `MONTH` |
+| `offset` | 是 | 提前/延后的偏移量(正数=提前,负数=延后;范围由 `unit` 决定):`MINUTE` ∈ {0, 5, 15, 30, -5, -15, -30};`HOUR` ∈ [-6, -1] ∪ [1, 6];`DAY` ∈ [-7, 7];`WEEK` ∈ [-7, -1] ∪ [1, 7];`MONTH` ∈ [-7, -1] ∪ [1, 7] |
+| `hour` | 是 | 触发小时 (0-23),默认 9 |
+| `minute` | 是 | 触发分钟 (0-59),默认 0 |
+| `condition_list` | 否 | 过滤条件数组,数组中每个元素为 AndCondition 结构,多个 AndCondition 之间为 OR 关系 |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/trigger-set-record.md b/skills/lark-base/references/workflow-steps/trigger-set-record.md
new file mode 100644
index 00000000..6b0bc3bf
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/trigger-set-record.md
@@ -0,0 +1,38 @@
+# SetRecordTrigger
+
+```json
+{
+ "table_name": "订单表",
+ "record_watch_conjunction": "and",
+ "record_watch_info": [ /* FieldCondition[] */ ],
+ "field_watch_info": [
+ { "field_name": "状态", "operator": "is", "value": [{ "value_type": "text", "value": "已发货" }] }
+ ],
+ "trigger_control_list": [],
+ "condition_list": null
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|----|------|
+| `table_name` | 是 | 监控的数据表名 |
+| `record_watch_conjunction` | 否 | 记录筛选组合方式:`and` / `or`,默认 `and` |
+| `record_watch_info` | 否 | 记录级过滤条件(修改前值匹配),为空则监听全部 |
+| `field_watch_info` | 是 | 字段级监控条件列表,至少一个 |
+| `trigger_control_list` | 否 | 触发控制,可选值:`pasteUpdate` / `automationBatchUpdate` / `syncUpdate` / `appendImport` |
+| `condition_list` | 否 | 过滤条件数组,数组中每个元素为 AndCondition 结构,多个 AndCondition 之间为 OR 关系 |
+
+`FieldWatchItem`:
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `field_name` | string | 监听字段名称 |
+| `operator` | string | 操作符(仅明确要求字段满足条件时填) |
+| `value` | ValueInfo[] | 触发值 |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-base/references/workflow-steps/trigger-timer.md b/skills/lark-base/references/workflow-steps/trigger-timer.md
new file mode 100644
index 00000000..b7f5f272
--- /dev/null
+++ b/skills/lark-base/references/workflow-steps/trigger-timer.md
@@ -0,0 +1,27 @@
+# TimerTrigger
+
+```json
+{
+ "rule": "WEEKLY",
+ "start_time": "2025-01-01 09:00",
+ "sub_unit": [1, 3, 5],
+ "is_never_end": true
+}
+```
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `rule` | 是 | `NO_REPEAT` / `DAILY` / `WEEKLY` / `MONTHLY` / `YEARLY` / `WORKDAY` / `CUSTOM` |
+| `start_time` | 否 | 开始时间,格式 `yyyy-MM-dd HH:mm` |
+| `interval` | 否 | 自定义间隔 [1,30](仅 CUSTOM) |
+| `unit` | 否 | 自定义单位:`SECOND` / `MINUTE` / `HOUR` / `DAY` / `WEEK` / `MONTH` / `YEAR` |
+| `sub_unit` | 否 | 子单位(`WEEKLY` 时为星期几数组 0-6,`MONTHLY` 时为几号数组 1-31) |
+| `end_time` | 否 | 结束时间 |
+| `is_never_end` | 否 | 是否永不结束 |
+
+---
+
+## 相关
+
+- 返回 [Workflow schema index](../lark-base-workflow-schema.md)
+- ValueInfo、ref、Condition、RecordFilterInfo 等公共结构见 [common-types-and-refs.md](common-types-and-refs.md)
diff --git a/skills/lark-drive/references/lark-drive-import.md b/skills/lark-drive/references/lark-drive-import.md
index 9b5d35aa..13968976 100644
--- a/skills/lark-drive/references/lark-drive-import.md
+++ b/skills/lark-drive/references/lark-drive-import.md
@@ -48,6 +48,7 @@ lark-cli drive +import --file ./data.csv --type bitable --folder-token
+# 成功后验证 ;不要拿返回中的导入任务 token 当作 Base token 复核
# 预览底层调用链(上传 -> 创建任务 -> 轮询)
lark-cli drive +import --file ./README.md --type docx --dry-run
@@ -72,7 +73,7 @@ lark-cli drive +import --file ./README.md --type docx --dry-run
2. 调用 `import_tasks` 接口发起导入任务,自动根据本地文件提取扩展名并构造挂载点(`mount_point`)参数
3. 自动轮询查询导入任务状态;如果在内置轮询窗口内完成,则直接返回导入结果;如果仍未完成,则返回 `ticket`、当前状态和后续查询命令
- **默认根目录行为**:不传 `--folder-token` 时,shortcut 会保留空的 `point.mount_key`,Lark Import API 会将其视为"导入到调用者根目录"。
-- **导入到已有 bitable**:当 `--type bitable` 且传了 `--target-token` 时,请求 body 中会增加一个 `token` 字段指向目标多维表格的 token,point 挂载点逻辑不变。数据会挂载到该已有多维表格中,而非创建新文档。
+- **导入到已有 bitable**:当 `--type bitable` 且传了 `--target-token` 时,请求 body 中会增加一个 `token` 字段指向目标多维表格。数据会挂载到该已有多维表格中,而非创建新文档;完成后用输出的 `verification_token`(即传入的 `--target-token`)复核,不要改用返回的导入任务 `token` 做 Base 查询。
### 支持的文件类型转换