diff --git a/shortcuts/base/base_dryrun_ops_test.go b/shortcuts/base/base_dryrun_ops_test.go index 3898826b7..80ca46299 100644 --- a/shortcuts/base/base_dryrun_ops_test.go +++ b/shortcuts/base/base_dryrun_ops_test.go @@ -63,12 +63,21 @@ func TestDryRunFieldOps(t *testing.T) { func TestDryRunRecordOps(t *testing.T) { ctx := context.Background() - listRT := newBaseTestRuntime( + listRT := newBaseTestRuntimeWithArrays( map[string]string{"base-token": "app_x", "table-id": "tbl_1", "view-id": "viw_1"}, + map[string][]string{"field-id": {"Name", "Age"}}, nil, map[string]int{"offset": -3, "limit": 500}, ) - assertDryRunContains(t, dryRunRecordList(ctx, listRT), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/records", "offset=0", "limit=200", "view_id=viw_1") + assertDryRunContains(t, dryRunRecordList(ctx, listRT), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/records", "offset=0", "limit=200", "view_id=viw_1", "field_id=Name", "field_id=Age") + + commaFieldRT := newBaseTestRuntimeWithArrays( + map[string]string{"base-token": "app_x", "table-id": "tbl_1"}, + map[string][]string{"field-id": {"A,B", "C"}}, + nil, + map[string]int{"limit": 1}, + ) + assertDryRunContains(t, dryRunRecordList(ctx, commaFieldRT), "limit=1", "offset=0", "field_id=A%2CB", "field_id=C") upsertCreateRT := newBaseTestRuntime( map[string]string{"base-token": "app_x", "table-id": "tbl_1", "json": `{"Name":"A"}`}, diff --git a/shortcuts/base/base_execute_test.go b/shortcuts/base/base_execute_test.go index 46ec996d9..78dca55d2 100644 --- a/shortcuts/base/base_execute_test.go +++ b/shortcuts/base/base_execute_test.go @@ -471,6 +471,52 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { } }) + t.Run("list with fields and view", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "field_id=Name&field_id=Age&limit=1&offset=0&view_id=vew_x", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "fields": []interface{}{"Name", "Age"}, + "record_id_list": []interface{}{"rec_fields"}, + "data": []interface{}{[]interface{}{"Alice", 18}}, + "total": 1, + }, + }, + }) + if err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--view-id", "vew_x", "--limit", "1", "--field-id", "Name", "--field-id", "Age"}, factory, stdout); err != nil { + t.Fatalf("err=%v", err) + } + if got := stdout.String(); !strings.Contains(got, `"rec_fields"`) || !strings.Contains(got, `"Alice"`) { + t.Fatalf("stdout=%s", got) + } + }) + + t.Run("list with comma field", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "field_id=A%2CB&field_id=C&limit=1&offset=0", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "fields": []interface{}{"A,B", "C"}, + "record_id_list": []interface{}{"rec_json_fields"}, + "data": []interface{}{[]interface{}{"value-1", "value-2"}}, + "total": 1, + }, + }, + }) + if err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--limit", "1", "--field-id", "A,B", "--field-id", "C"}, factory, stdout); err != nil { + t.Fatalf("err=%v", err) + } + if got := stdout.String(); !strings.Contains(got, `"A,B"`) || !strings.Contains(got, `"rec_json_fields"`) { + t.Fatalf("stdout=%s", got) + } + }) + t.Run("list new shape", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) reg.Register(&httpmock.Stub{ @@ -494,6 +540,22 @@ 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.Fatalf("err=%v", err) + } + }) + + t.Run("list legacy fields flag rejected 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") { + t.Fatalf("err=%v", err) + } + }) + t.Run("get", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) reg.Register(&httpmock.Stub{ diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index a6f1c61d0..13cb2df25 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -18,10 +18,17 @@ import ( ) func newBaseTestRuntime(stringFlags map[string]string, boolFlags map[string]bool, intFlags map[string]int) *common.RuntimeContext { + return newBaseTestRuntimeWithArrays(stringFlags, nil, boolFlags, intFlags) +} + +func newBaseTestRuntimeWithArrays(stringFlags map[string]string, stringArrayFlags map[string][]string, boolFlags map[string]bool, intFlags map[string]int) *common.RuntimeContext { cmd := &cobra.Command{Use: "test"} for name := range stringFlags { cmd.Flags().String(name, "", "") } + for name := range stringArrayFlags { + cmd.Flags().StringArray(name, nil, "") + } for name := range boolFlags { cmd.Flags().Bool(name, false, "") } @@ -32,6 +39,11 @@ func newBaseTestRuntime(stringFlags map[string]string, boolFlags map[string]bool for name, value := range stringFlags { _ = cmd.Flags().Set(name, value) } + for name, values := range stringArrayFlags { + for _, value := range values { + _ = cmd.Flags().Set(name, value) + } + } for name, value := range boolFlags { if value { _ = cmd.Flags().Set(name, "true") @@ -236,10 +248,10 @@ func TestBaseTableValidate(t *testing.T) { func TestBaseRecordValidate(t *testing.T) { ctx := context.Background() if BaseRecordList.Validate != nil { - t.Fatalf("record list validate should be nil after removing --fields") + t.Fatalf("record list validate should be nil for repeatable --field-id") } if BaseRecordGet.Validate != nil { - t.Fatalf("record get validate should be nil after removing --fields") + t.Fatalf("record get validate should be nil") } if err := BaseRecordUpsert.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "tbl_1", "json": `{"Name":"A"}`}, nil, nil)); err != nil { t.Fatalf("upsert validate err=%v", err) diff --git a/shortcuts/base/helpers.go b/shortcuts/base/helpers.go index 9032ab5a2..2d034c78e 100644 --- a/shortcuts/base/helpers.go +++ b/shortcuts/base/helpers.go @@ -368,7 +368,18 @@ func baseV3Path(parts ...string) string { func baseV3Raw(runtime *common.RuntimeContext, method, path string, params map[string]interface{}, data interface{}) (map[string]interface{}, error) { queryParams := make(larkcore.QueryParams) for k, v := range params { - queryParams.Set(k, fmt.Sprintf("%v", v)) + switch val := v.(type) { + case []string: + for _, item := range val { + queryParams.Add(k, item) + } + case []interface{}: + for _, item := range val { + queryParams.Add(k, fmt.Sprintf("%v", item)) + } + default: + queryParams.Set(k, fmt.Sprintf("%v", v)) + } } req := &larkcore.ApiReq{ HttpMethod: strings.ToUpper(method), diff --git a/shortcuts/base/record_list.go b/shortcuts/base/record_list.go index 815cfb285..eabfe82dc 100644 --- a/shortcuts/base/record_list.go +++ b/shortcuts/base/record_list.go @@ -19,6 +19,7 @@ var BaseRecordList = common.Shortcut{ Flags: []common.Flag{ baseTokenFlag(true), tableRefFlag(true), + {Name: "field-id", Type: "string_array", Desc: "field ID or field name to include (repeatable)"}, {Name: "view-id", Desc: "view ID"}, {Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"}, {Name: "limit", Type: "int", Default: "100", Desc: "pagination size"}, diff --git a/shortcuts/base/record_ops.go b/shortcuts/base/record_ops.go index 280b1c580..d6407625c 100644 --- a/shortcuts/base/record_ops.go +++ b/shortcuts/base/record_ops.go @@ -5,6 +5,8 @@ package base import ( "context" + "net/url" + "strconv" "github.com/larksuite/cli/shortcuts/common" ) @@ -15,13 +17,18 @@ func dryRunRecordList(_ context.Context, runtime *common.RuntimeContext) *common offset = 0 } limit := common.ParseIntBounded(runtime, "limit", 1, 200) - params := map[string]interface{}{"offset": offset, "limit": limit} + params := url.Values{} + params.Set("offset", strconv.Itoa(offset)) + params.Set("limit", strconv.Itoa(limit)) + for _, field := range recordListFields(runtime) { + params.Add("field_id", field) + } if viewID := runtime.Str("view-id"); viewID != "" { - params["view_id"] = viewID + params.Set("view_id", viewID) } + path := "/open-apis/base/v3/bases/:base_token/tables/:table_id/records?" + params.Encode() return common.NewDryRunAPI(). - GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/records"). - Params(params). + GET(path). Set("base_token", runtime.Str("base-token")). Set("table_id", baseTableID(runtime)) } @@ -78,6 +85,10 @@ func validateRecordJSON(runtime *common.RuntimeContext) error { return nil } +func recordListFields(runtime *common.RuntimeContext) []string { + return runtime.StrArray("field-id") +} + func executeRecordList(runtime *common.RuntimeContext) error { offset := runtime.Int("offset") if offset < 0 { @@ -85,6 +96,10 @@ func executeRecordList(runtime *common.RuntimeContext) error { } limit := common.ParseIntBounded(runtime, "limit", 1, 200) params := map[string]interface{}{"offset": offset, "limit": limit} + fields := recordListFields(runtime) + if len(fields) > 0 { + params["field_id"] = fields + } if viewID := runtime.Str("view-id"); viewID != "" { params["view_id"] = viewID } diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index 23f4abc87..b431dc06a 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -155,7 +155,8 @@ metadata: - **Base token 口径统一**:统一使用 `--base-token` - **`+xxx-list` 调用纪律**:`+table-list / +field-list / +record-list / +view-list / +record-history-list / +role-list / +dashboard-list / +dashboard-block-list / +workflow-list` 禁止并发调用;批量执行时只能串行 -- **`+record-list` 分页规则**:`--limit` 最大 `200`。先拉首批并检查返回 `has_more`;仅当 `has_more=true` 且用户明确需要更多数据(如“全部导出/全量明细/继续下一页”)时再继续翻页。用户只要样例或前 N 条时,不要继续拉全量 +- **`+record-list` 分页与 limit**:`--limit` 最大 `200`。先拉首批并检查 `has_more`;仅当 `has_more=true` 且用户明确需要更多数据(如“全部导出/全量明细/继续下一页”)时再按 `offset` 递增翻页,禁止单次传超过 `200` +- **记录读取字段筛选**:`+record-list` 支持重复传参 --field-id 做字段筛选 - **字段可写性先判断**:存储字段才可写;公式 / lookup / 系统字段默认只读,写记录时应跳过 - **公式能力要主动想到**:用户说“算一下”“生成标签”“判断是否异常”“跨表汇总”“按日期差预警”时,要先判断是否应该建公式字段,而不是只返回手工分析方案 - **lookup 不是默认首选**:lookup 只在用户明确要求或确实更适合固定查找模型时使用;常规计算、跨表聚合和条件判断优先 formula diff --git a/skills/lark-base/references/lark-base-record-get.md b/skills/lark-base/references/lark-base-record-get.md index 663bd5cc9..6499126bd 100644 --- a/skills/lark-base/references/lark-base-record-get.md +++ b/skills/lark-base/references/lark-base-record-get.md @@ -11,12 +11,6 @@ lark-cli base +record-get \ --base-token app_xxx \ --table-id tbl_xxx \ --record-id rec_xxx - -lark-cli base +record-get \ - --base-token app_xxx \ - --table-id tbl_xxx \ - --record-id rec_xxx \ - --fields 项目名称,状态 ``` ## 参数 @@ -26,7 +20,6 @@ lark-cli base +record-get \ | `--base-token ` | 是 | Base Token | | `--table-id ` | 是 | 表 ID 或表名 | | `--record-id ` | 是 | 记录 ID | -| `--fields ` | 否 | 字段名 CSV,或 JSON 字符串数组 | ## API 入参详情 @@ -38,8 +31,7 @@ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id ## 返回重点 -- 返回 `record` 和 `raw`。 -- `record` 是裁剪后的单条结果;`raw` 保留接口完整响应。 +- 成功时直接返回接口 `data` 字段内容。 ## 参考 diff --git a/skills/lark-base/references/lark-base-record-list.md b/skills/lark-base/references/lark-base-record-list.md index faaca74ff..f785a85f9 100644 --- a/skills/lark-base/references/lark-base-record-list.md +++ b/skills/lark-base/references/lark-base-record-list.md @@ -2,13 +2,19 @@ > **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 -分页列出一张表里的记录;可按视图过滤。 +分页列出一张表里的记录;可按视图过滤,也可按字段裁剪返回列。 ## 返回关键字段 | 字段 | 类型 | 说明 | |------|------|------| | `has_more` | boolean | 是否还有下一页数据;`true` 表示可继续翻页,`false` 表示已到末页 | +| `query_context.record_scope` | string | 记录范围:`all_records`(全表)或 `view_filtered_records`(按视图过滤) | +| `query_context.field_scope` | string | 字段范围:`selected_fields`(显式传 `--field-id`)/ `view_visible_fields`(未传 `--field-id` 且按视图可见字段)/ `all_fields`(未传 `--field-id` 且无视图限制) | + +## 字段返回优先级 + +- `query_context.field_scope` 的优先级为:`selected_fields`(explicit `--field-id`) > `view_visible_fields`(view visible fields) > `all_fields`(table all fields)。 ## 按需翻页规则 @@ -33,6 +39,8 @@ lark-cli base +record-list \ --base-token app_xxx \ --table-id tbl_xxx \ --view-id viw_xxx \ + --field-id fld_status \ + --field-id 项目名称 \ --offset 0 \ --limit 50 ``` @@ -44,6 +52,7 @@ lark-cli base +record-list \ | `--base-token ` | 是 | Base Token | | `--table-id ` | 是 | 表 ID 或表名 | | `--view-id ` | 否 | 视图 ID;传入后只读该视图结果 | +| `--field-id ` | 否 | 字段 ID 或字段名;可重复传入多个 `--field-id` 裁剪返回列 | | `--offset ` | 否 | 分页偏移,默认 `0` | | `--limit ` | 否 | 分页大小,默认 `100`,范围 `1-200`(最大 `200`,超过会报错) | @@ -55,7 +64,7 @@ lark-cli base +record-list \ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records ``` -- 查询参数会附带 `view_id / offset / limit`。 +- 查询参数会附带 `view_id / field_id(repeatable) / offset / limit`。 ## 坑点 @@ -63,6 +72,7 @@ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records - ⚠️ `+record-list` 禁止并发调用;批量拉多个视图或多张表时必须串行。 - ⚠️ `--limit` 最大 `200`,不要传超过 `200` 的值。 - ⚠️ 分页时优先根据返回的 `has_more` 判断是否继续请求,不要盲目预拉全量数据。 +- ⚠️ `--field-id` 接受字段 ID 或字段名。 - ⚠️ 复杂筛选优先落到视图里,再用 `--view-id` 读取。 ## 参考 diff --git a/skills/lark-base/references/lark-base-record.md b/skills/lark-base/references/lark-base-record.md index 15b29393f..1b3e59f27 100644 --- a/skills/lark-base/references/lark-base-record.md +++ b/skills/lark-base/references/lark-base-record.md @@ -18,5 +18,6 @@ record 相关命令索引。 - 聚合页只保留目录职责;每个命令的详细说明请进入对应单命令文档。 - 所有 `+xxx-list` 调用都必须串行执行;若要批量跑多个 list 请求,只能串行执行。 +- `+record-list` 支持重复传参 `--field-id` 做字段筛选。 - 写记录 JSON 前优先阅读 [lark-base-shortcut-record-value.md](lark-base-shortcut-record-value.md)。 - 本地文件写入附件字段时,必须使用 `+record-upload-attachment`。