From 8e733b52d995d28b9d61916e4a699a022e296e41 Mon Sep 17 00:00:00 2001 From: zgz2048 Date: Mon, 30 Mar 2026 18:17:57 +0800 Subject: [PATCH 1/5] feat(base): add record field filters --- shortcuts/base/base_dryrun_ops_test.go | 14 ++- shortcuts/base/base_execute_test.go | 110 ++++++++++++++++++ shortcuts/base/base_shortcuts_test.go | 16 ++- shortcuts/base/helpers.go | 13 ++- shortcuts/base/record_get.go | 1 + shortcuts/base/record_list.go | 1 + shortcuts/base/record_ops.go | 22 +++- skills/lark-base/SKILL.md | 3 +- .../references/lark-base-record-get.md | 9 +- .../references/lark-base-record-list.md | 8 +- .../lark-base/references/lark-base-record.md | 1 + 11 files changed, 185 insertions(+), 13 deletions(-) diff --git a/shortcuts/base/base_dryrun_ops_test.go b/shortcuts/base/base_dryrun_ops_test.go index 3898826b7..f62ced503 100644 --- a/shortcuts/base/base_dryrun_ops_test.go +++ b/shortcuts/base/base_dryrun_ops_test.go @@ -63,21 +63,31 @@ 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": {"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") + commaFieldRT := newBaseTestRuntimeWithArrays( + map[string]string{"base-token": "app_x", "table-id": "tbl_1"}, + map[string][]string{"field": {"A,B", "C"}}, + nil, + map[string]int{"limit": 1}, + ) + assertDryRunContains(t, dryRunRecordList(ctx, commaFieldRT), "limit=1", "offset=0") + upsertCreateRT := newBaseTestRuntime( map[string]string{"base-token": "app_x", "table-id": "tbl_1", "json": `{"Name":"A"}`}, nil, nil, ) assertDryRunContains(t, dryRunRecordUpsert(ctx, upsertCreateRT), "POST /open-apis/base/v3/bases/app_x/tables/tbl_1/records") - rt := newBaseTestRuntime( + rt := newBaseTestRuntimeWithArrays( map[string]string{"base-token": "app_x", "table-id": "tbl_1", "record-id": "rec_1", "json": `{"Name":"B"}`}, + map[string][]string{"field": {"Name", "Age"}}, nil, map[string]int{"max-version": 11, "page-size": 30}, ) diff --git a/shortcuts/base/base_execute_test.go b/shortcuts/base/base_execute_test.go index 46ec996d9..9064c8974 100644 --- a/shortcuts/base/base_execute_test.go +++ b/shortcuts/base/base_execute_test.go @@ -471,6 +471,54 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { } }) + t.Run("list with fields and view", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + registerTokenStub(reg) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "field=Name&field=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", "Name", "--field", "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) + registerTokenStub(reg) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "field=A%2CB&field=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", "A,B", "--field", "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 +542,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{ @@ -516,6 +580,52 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { } }) + t.Run("get with fields", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + registerTokenStub(reg) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/rec_fields?field=Name&field=Age", + 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}}, + }, + }, + }) + if err := runShortcut(t, BaseRecordGet, []string{"+record-get", "--base-token", "app_x", "--table-id", "tbl_x", "--record-id", "rec_fields", "--field", "Name", "--field", "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("get with comma field", func(t *testing.T) { + factory, stdout, reg := newExecuteFactory(t) + registerTokenStub(reg) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/rec_comma?field=A%2CB", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "fields": []interface{}{"A,B"}, + "record_id_list": []interface{}{"rec_comma"}, + "data": []interface{}{[]interface{}{"value-1"}}, + }, + }, + }) + if err := runShortcut(t, BaseRecordGet, []string{"+record-get", "--base-token", "app_x", "--table-id", "tbl_x", "--record-id", "rec_comma", "--field", "A,B"}, factory, stdout); err != nil { + t.Fatalf("err=%v", err) + } + if got := stdout.String(); !strings.Contains(got, `"A,B"`) || !strings.Contains(got, `"rec_comma"`) { + t.Fatalf("stdout=%s", got) + } + }) + t.Run("get passthrough fallback", 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..6c8e5faa7 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") } if BaseRecordGet.Validate != nil { - t.Fatalf("record get validate should be nil after removing --fields") + t.Fatalf("record get validate should be nil for repeatable --field") } 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_get.go b/shortcuts/base/record_get.go index b54c5466d..ec0eac3bb 100644 --- a/shortcuts/base/record_get.go +++ b/shortcuts/base/record_get.go @@ -20,6 +20,7 @@ var BaseRecordGet = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), recordRefFlag(true), + {Name: "field", Type: "string_array", Desc: "field ID or field name to include (repeatable)"}, }, DryRun: dryRunRecordGet, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/base/record_list.go b/shortcuts/base/record_list.go index 815cfb285..a76bd478c 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", 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..9c0f9a884 100644 --- a/shortcuts/base/record_ops.go +++ b/shortcuts/base/record_ops.go @@ -16,6 +16,9 @@ func dryRunRecordList(_ context.Context, runtime *common.RuntimeContext) *common } limit := common.ParseIntBounded(runtime, "limit", 1, 200) params := map[string]interface{}{"offset": offset, "limit": limit} + if fields := recordFields(runtime); len(fields) > 0 { + params["field"] = fields + } if viewID := runtime.Str("view-id"); viewID != "" { params["view_id"] = viewID } @@ -27,8 +30,13 @@ func dryRunRecordList(_ context.Context, runtime *common.RuntimeContext) *common } func dryRunRecordGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + params := map[string]interface{}{} + if fields := recordFields(runtime); len(fields) > 0 { + params["field"] = fields + } return common.NewDryRunAPI(). GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id"). + Params(params). Set("base_token", runtime.Str("base-token")). Set("table_id", baseTableID(runtime)). Set("record_id", runtime.Str("record-id")) @@ -78,6 +86,10 @@ func validateRecordJSON(runtime *common.RuntimeContext) error { return nil } +func recordFields(runtime *common.RuntimeContext) []string { + return runtime.StrArray("field") +} + func executeRecordList(runtime *common.RuntimeContext) error { offset := runtime.Int("offset") if offset < 0 { @@ -85,6 +97,10 @@ func executeRecordList(runtime *common.RuntimeContext) error { } limit := common.ParseIntBounded(runtime, "limit", 1, 200) params := map[string]interface{}{"offset": offset, "limit": limit} + fields := recordFields(runtime) + if len(fields) > 0 { + params["field"] = fields + } if viewID := runtime.Str("view-id"); viewID != "" { params["view_id"] = viewID } @@ -97,7 +113,11 @@ func executeRecordList(runtime *common.RuntimeContext) error { } func executeRecordGet(runtime *common.RuntimeContext) error { - data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "records", runtime.Str("record-id")), nil, nil) + params := map[string]interface{}{} + if fields := recordFields(runtime); len(fields) > 0 { + params["field"] = fields + } + data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "records", runtime.Str("record-id")), params, nil) if err != nil { return err } diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index 23f4abc87..ed329ce3c 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 / +record-get` 统一使用 repeatable `--field`;每次传一个字段,接受字段 ID 或字段名,例如 `--field fld_status --field 项目名称`;不指定字段时默认返回所有字段 - **字段可写性先判断**:存储字段才可写;公式 / 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..730d1b871 100644 --- a/skills/lark-base/references/lark-base-record-get.md +++ b/skills/lark-base/references/lark-base-record-get.md @@ -16,7 +16,8 @@ lark-cli base +record-get \ --base-token app_xxx \ --table-id tbl_xxx \ --record-id rec_xxx \ - --fields 项目名称,状态 + --field fld_status \ + --field 项目名称 ``` ## 参数 @@ -26,7 +27,7 @@ lark-cli base +record-get \ | `--base-token ` | 是 | Base Token | | `--table-id ` | 是 | 表 ID 或表名 | | `--record-id ` | 是 | 记录 ID | -| `--fields ` | 否 | 字段名 CSV,或 JSON 字符串数组 | +| `--field ` | 否 | 字段 ID 或字段名;可重复传入多个 `--field` 裁剪返回列;不指定时默认返回所有字段 | ## API 入参详情 @@ -38,8 +39,8 @@ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id ## 返回重点 -- 返回 `record` 和 `raw`。 -- `record` 是裁剪后的单条结果;`raw` 保留接口完整响应。 +- 成功时直接返回接口 `data` 字段内容。 +- 传了 `--field` 时,由服务端按字段裁剪返回结果。 ## 参考 diff --git a/skills/lark-base/references/lark-base-record-list.md b/skills/lark-base/references/lark-base-record-list.md index faaca74ff..b00aa1282 100644 --- a/skills/lark-base/references/lark-base-record-list.md +++ b/skills/lark-base/references/lark-base-record-list.md @@ -2,7 +2,7 @@ > **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 -分页列出一张表里的记录;可按视图过滤。 +分页列出一张表里的记录;可按视图过滤,也可按字段裁剪返回列。 ## 返回关键字段 @@ -33,6 +33,8 @@ lark-cli base +record-list \ --base-token app_xxx \ --table-id tbl_xxx \ --view-id viw_xxx \ + --field fld_status \ + --field 项目名称 \ --offset 0 \ --limit 50 ``` @@ -44,6 +46,7 @@ lark-cli base +record-list \ | `--base-token ` | 是 | Base Token | | `--table-id ` | 是 | 表 ID 或表名 | | `--view-id ` | 否 | 视图 ID;传入后只读该视图结果 | +| `--field ` | 否 | 字段 ID 或字段名;可重复传入多个 `--field` 裁剪返回列 | | `--offset ` | 否 | 分页偏移,默认 `0` | | `--limit ` | 否 | 分页大小,默认 `100`,范围 `1-200`(最大 `200`,超过会报错) | @@ -55,7 +58,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(repeatable) / offset / limit`。 ## 坑点 @@ -63,6 +66,7 @@ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records - ⚠️ `+record-list` 禁止并发调用;批量拉多个视图或多张表时必须串行。 - ⚠️ `--limit` 最大 `200`,不要传超过 `200` 的值。 - ⚠️ 分页时优先根据返回的 `has_more` 判断是否继续请求,不要盲目预拉全量数据。 +- ⚠️ `--field` 接受字段 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..300ce995e 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 / +record-get` 的字段筛选统一使用 repeatable `--field`;每次传一个字段,可重复多次传入多个字段,接受字段 ID 或字段名。 - 写记录 JSON 前优先阅读 [lark-base-shortcut-record-value.md](lark-base-shortcut-record-value.md)。 - 本地文件写入附件字段时,必须使用 `+record-upload-attachment`。 From 045d475bc597dcc232181a3c124bb3452f15a098 Mon Sep 17 00:00:00 2001 From: kongenpei Date: Wed, 8 Apr 2026 15:36:07 +0800 Subject: [PATCH 2/5] fix(base): align record field filter flags with OpenAPI params --- shortcuts/base/base_dryrun_ops_test.go | 6 +++--- shortcuts/base/base_execute_test.go | 16 ++++++---------- shortcuts/base/base_shortcuts_test.go | 4 ++-- shortcuts/base/record_get.go | 2 +- shortcuts/base/record_list.go | 2 +- shortcuts/base/record_ops.go | 6 +++--- 6 files changed, 16 insertions(+), 20 deletions(-) diff --git a/shortcuts/base/base_dryrun_ops_test.go b/shortcuts/base/base_dryrun_ops_test.go index f62ced503..214705e24 100644 --- a/shortcuts/base/base_dryrun_ops_test.go +++ b/shortcuts/base/base_dryrun_ops_test.go @@ -65,7 +65,7 @@ func TestDryRunRecordOps(t *testing.T) { listRT := newBaseTestRuntimeWithArrays( map[string]string{"base-token": "app_x", "table-id": "tbl_1", "view-id": "viw_1"}, - map[string][]string{"field": {"Name", "Age"}}, + map[string][]string{"field-id": {"Name", "Age"}}, nil, map[string]int{"offset": -3, "limit": 500}, ) @@ -73,7 +73,7 @@ func TestDryRunRecordOps(t *testing.T) { commaFieldRT := newBaseTestRuntimeWithArrays( map[string]string{"base-token": "app_x", "table-id": "tbl_1"}, - map[string][]string{"field": {"A,B", "C"}}, + map[string][]string{"field-id": {"A,B", "C"}}, nil, map[string]int{"limit": 1}, ) @@ -87,7 +87,7 @@ func TestDryRunRecordOps(t *testing.T) { rt := newBaseTestRuntimeWithArrays( map[string]string{"base-token": "app_x", "table-id": "tbl_1", "record-id": "rec_1", "json": `{"Name":"B"}`}, - map[string][]string{"field": {"Name", "Age"}}, + map[string][]string{"field-id": {"Name", "Age"}}, nil, map[string]int{"max-version": 11, "page-size": 30}, ) diff --git a/shortcuts/base/base_execute_test.go b/shortcuts/base/base_execute_test.go index 9064c8974..b8e5e937e 100644 --- a/shortcuts/base/base_execute_test.go +++ b/shortcuts/base/base_execute_test.go @@ -473,10 +473,9 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { t.Run("list with fields and view", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) - registerTokenStub(reg) reg.Register(&httpmock.Stub{ Method: "GET", - URL: "field=Name&field=Age&limit=1&offset=0&view_id=vew_x", + 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{}{ @@ -487,7 +486,7 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { }, }, }) - if err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--view-id", "vew_x", "--limit", "1", "--field", "Name", "--field", "Age"}, factory, stdout); err != nil { + 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"`) { @@ -497,10 +496,9 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { t.Run("list with comma field", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) - registerTokenStub(reg) reg.Register(&httpmock.Stub{ Method: "GET", - URL: "field=A%2CB&field=C&limit=1&offset=0", + URL: "field_id=A%2CB&field_id=C&limit=1&offset=0", Body: map[string]interface{}{ "code": 0, "data": map[string]interface{}{ @@ -511,7 +509,7 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { }, }, }) - if err := runShortcut(t, BaseRecordList, []string{"+record-list", "--base-token", "app_x", "--table-id", "tbl_x", "--limit", "1", "--field", "A,B", "--field", "C"}, factory, stdout); err != nil { + 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"`) { @@ -582,7 +580,6 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { t.Run("get with fields", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) - registerTokenStub(reg) reg.Register(&httpmock.Stub{ Method: "GET", URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/rec_fields?field=Name&field=Age", @@ -595,7 +592,7 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { }, }, }) - if err := runShortcut(t, BaseRecordGet, []string{"+record-get", "--base-token", "app_x", "--table-id", "tbl_x", "--record-id", "rec_fields", "--field", "Name", "--field", "Age"}, factory, stdout); err != nil { + if err := runShortcut(t, BaseRecordGet, []string{"+record-get", "--base-token", "app_x", "--table-id", "tbl_x", "--record-id", "rec_fields", "--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"`) { @@ -605,7 +602,6 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { t.Run("get with comma field", func(t *testing.T) { factory, stdout, reg := newExecuteFactory(t) - registerTokenStub(reg) reg.Register(&httpmock.Stub{ Method: "GET", URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/rec_comma?field=A%2CB", @@ -618,7 +614,7 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { }, }, }) - if err := runShortcut(t, BaseRecordGet, []string{"+record-get", "--base-token", "app_x", "--table-id", "tbl_x", "--record-id", "rec_comma", "--field", "A,B"}, factory, stdout); err != nil { + if err := runShortcut(t, BaseRecordGet, []string{"+record-get", "--base-token", "app_x", "--table-id", "tbl_x", "--record-id", "rec_comma", "--field-id", "A,B"}, factory, stdout); err != nil { t.Fatalf("err=%v", err) } if got := stdout.String(); !strings.Contains(got, `"A,B"`) || !strings.Contains(got, `"rec_comma"`) { diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index 6c8e5faa7..a8bdc5da6 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -248,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 for repeatable --field") + t.Fatalf("record list validate should be nil for repeatable --field-id") } if BaseRecordGet.Validate != nil { - t.Fatalf("record get validate should be nil for repeatable --field") + t.Fatalf("record get validate should be nil for repeatable --field-id") } 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/record_get.go b/shortcuts/base/record_get.go index ec0eac3bb..e56868acb 100644 --- a/shortcuts/base/record_get.go +++ b/shortcuts/base/record_get.go @@ -20,7 +20,7 @@ var BaseRecordGet = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), recordRefFlag(true), - {Name: "field", Type: "string_array", Desc: "field ID or field name to include (repeatable)"}, + {Name: "field-id", Type: "string_array", Desc: "field ID or field name to include (repeatable)"}, }, DryRun: dryRunRecordGet, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/base/record_list.go b/shortcuts/base/record_list.go index a76bd478c..eabfe82dc 100644 --- a/shortcuts/base/record_list.go +++ b/shortcuts/base/record_list.go @@ -19,7 +19,7 @@ var BaseRecordList = common.Shortcut{ Flags: []common.Flag{ baseTokenFlag(true), tableRefFlag(true), - {Name: "field", Type: "string_array", Desc: "field ID or field name to include (repeatable)"}, + {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 9c0f9a884..93fc336fc 100644 --- a/shortcuts/base/record_ops.go +++ b/shortcuts/base/record_ops.go @@ -17,7 +17,7 @@ func dryRunRecordList(_ context.Context, runtime *common.RuntimeContext) *common limit := common.ParseIntBounded(runtime, "limit", 1, 200) params := map[string]interface{}{"offset": offset, "limit": limit} if fields := recordFields(runtime); len(fields) > 0 { - params["field"] = fields + params["field_id"] = fields } if viewID := runtime.Str("view-id"); viewID != "" { params["view_id"] = viewID @@ -87,7 +87,7 @@ func validateRecordJSON(runtime *common.RuntimeContext) error { } func recordFields(runtime *common.RuntimeContext) []string { - return runtime.StrArray("field") + return runtime.StrArray("field-id") } func executeRecordList(runtime *common.RuntimeContext) error { @@ -99,7 +99,7 @@ func executeRecordList(runtime *common.RuntimeContext) error { params := map[string]interface{}{"offset": offset, "limit": limit} fields := recordFields(runtime) if len(fields) > 0 { - params["field"] = fields + params["field_id"] = fields } if viewID := runtime.Str("view-id"); viewID != "" { params["view_id"] = viewID From db0177990ec43de455ae994e7c30a2d1c2dffce1 Mon Sep 17 00:00:00 2001 From: kongenpei Date: Wed, 8 Apr 2026 16:27:16 +0800 Subject: [PATCH 3/5] fix: scope record dry-run field filters and align docs --- shortcuts/base/base_dryrun_ops_test.go | 6 ++--- shortcuts/base/record_ops.go | 27 ++++++++++++------- skills/lark-base/SKILL.md | 2 +- .../references/lark-base-record-get.md | 8 +++--- .../references/lark-base-record-list.md | 10 +++---- .../lark-base/references/lark-base-record.md | 2 +- 6 files changed, 31 insertions(+), 24 deletions(-) diff --git a/shortcuts/base/base_dryrun_ops_test.go b/shortcuts/base/base_dryrun_ops_test.go index 214705e24..8e13cf302 100644 --- a/shortcuts/base/base_dryrun_ops_test.go +++ b/shortcuts/base/base_dryrun_ops_test.go @@ -69,7 +69,7 @@ func TestDryRunRecordOps(t *testing.T) { 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"}, @@ -77,7 +77,7 @@ func TestDryRunRecordOps(t *testing.T) { nil, map[string]int{"limit": 1}, ) - assertDryRunContains(t, dryRunRecordList(ctx, commaFieldRT), "limit=1", "offset=0") + 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"}`}, @@ -91,7 +91,7 @@ func TestDryRunRecordOps(t *testing.T) { nil, map[string]int{"max-version": 11, "page-size": 30}, ) - assertDryRunContains(t, dryRunRecordGet(ctx, rt), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/records/rec_1") + assertDryRunContains(t, dryRunRecordGet(ctx, rt), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/records/rec_1", "field=Name", "field=Age") assertDryRunContains(t, dryRunRecordUpsert(ctx, rt), "PATCH /open-apis/base/v3/bases/app_x/tables/tbl_1/records/rec_1") assertDryRunContains(t, dryRunRecordDelete(ctx, rt), "DELETE /open-apis/base/v3/bases/app_x/tables/tbl_1/records/rec_1") assertDryRunContains(t, dryRunRecordHistoryList(ctx, rt), "GET /open-apis/base/v3/bases/app_x/record_history", "max_version=11", "page_size=30", "record_id=rec_1", "table_id=tbl_1") diff --git a/shortcuts/base/record_ops.go b/shortcuts/base/record_ops.go index 93fc336fc..8e4765caa 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,28 +17,33 @@ 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} - if fields := recordFields(runtime); len(fields) > 0 { - params["field_id"] = fields + params := url.Values{} + params.Set("offset", strconv.Itoa(offset)) + params.Set("limit", strconv.Itoa(limit)) + for _, field := range recordFields(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)) } func dryRunRecordGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { - params := map[string]interface{}{} + path := "/open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id" if fields := recordFields(runtime); len(fields) > 0 { - params["field"] = fields + params := url.Values{} + for _, field := range fields { + params.Add("field", field) + } + path += "?" + params.Encode() } return common.NewDryRunAPI(). - GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id"). - Params(params). + GET(path). Set("base_token", runtime.Str("base-token")). Set("table_id", baseTableID(runtime)). Set("record_id", runtime.Str("record-id")) diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index ed329ce3c..92ef1720c 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -156,7 +156,7 @@ 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**:`--limit` 最大 `200`。先拉首批并检查 `has_more`;仅当 `has_more=true` 且用户明确需要更多数据(如“全部导出/全量明细/继续下一页”)时再按 `offset` 递增翻页,禁止单次传超过 `200` -- **记录读取字段筛选**:`+record-list / +record-get` 统一使用 repeatable `--field`;每次传一个字段,接受字段 ID 或字段名,例如 `--field fld_status --field 项目名称`;不指定字段时默认返回所有字段 +- **记录读取字段筛选**:`+record-list / +record-get` 统一使用 repeatable `--field-id`;每次传一个字段,接受字段 ID 或字段名,例如 `--field-id fld_status --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 730d1b871..396a69d13 100644 --- a/skills/lark-base/references/lark-base-record-get.md +++ b/skills/lark-base/references/lark-base-record-get.md @@ -16,8 +16,8 @@ lark-cli base +record-get \ --base-token app_xxx \ --table-id tbl_xxx \ --record-id rec_xxx \ - --field fld_status \ - --field 项目名称 + --field-id fld_status \ + --field-id 项目名称 ``` ## 参数 @@ -27,7 +27,7 @@ lark-cli base +record-get \ | `--base-token ` | 是 | Base Token | | `--table-id ` | 是 | 表 ID 或表名 | | `--record-id ` | 是 | 记录 ID | -| `--field ` | 否 | 字段 ID 或字段名;可重复传入多个 `--field` 裁剪返回列;不指定时默认返回所有字段 | +| `--field-id ` | 否 | 字段 ID 或字段名;可重复传入多个 `--field-id` 裁剪返回列;不指定时默认返回所有字段 | ## API 入参详情 @@ -40,7 +40,7 @@ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id ## 返回重点 - 成功时直接返回接口 `data` 字段内容。 -- 传了 `--field` 时,由服务端按字段裁剪返回结果。 +- 传了 `--field-id` 时,由服务端按字段裁剪返回结果。 ## 参考 diff --git a/skills/lark-base/references/lark-base-record-list.md b/skills/lark-base/references/lark-base-record-list.md index b00aa1282..fa3866925 100644 --- a/skills/lark-base/references/lark-base-record-list.md +++ b/skills/lark-base/references/lark-base-record-list.md @@ -33,8 +33,8 @@ lark-cli base +record-list \ --base-token app_xxx \ --table-id tbl_xxx \ --view-id viw_xxx \ - --field fld_status \ - --field 项目名称 \ + --field-id fld_status \ + --field-id 项目名称 \ --offset 0 \ --limit 50 ``` @@ -46,7 +46,7 @@ lark-cli base +record-list \ | `--base-token ` | 是 | Base Token | | `--table-id ` | 是 | 表 ID 或表名 | | `--view-id ` | 否 | 视图 ID;传入后只读该视图结果 | -| `--field ` | 否 | 字段 ID 或字段名;可重复传入多个 `--field` 裁剪返回列 | +| `--field-id ` | 否 | 字段 ID 或字段名;可重复传入多个 `--field-id` 裁剪返回列 | | `--offset ` | 否 | 分页偏移,默认 `0` | | `--limit ` | 否 | 分页大小,默认 `100`,范围 `1-200`(最大 `200`,超过会报错) | @@ -58,7 +58,7 @@ lark-cli base +record-list \ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records ``` -- 查询参数会附带 `view_id / field(repeatable) / offset / limit`。 +- 查询参数会附带 `view_id / field_id(repeatable) / offset / limit`。 ## 坑点 @@ -66,7 +66,7 @@ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records - ⚠️ `+record-list` 禁止并发调用;批量拉多个视图或多张表时必须串行。 - ⚠️ `--limit` 最大 `200`,不要传超过 `200` 的值。 - ⚠️ 分页时优先根据返回的 `has_more` 判断是否继续请求,不要盲目预拉全量数据。 -- ⚠️ `--field` 接受字段 ID 或字段名。 +- ⚠️ `--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 300ce995e..10aeacb7c 100644 --- a/skills/lark-base/references/lark-base-record.md +++ b/skills/lark-base/references/lark-base-record.md @@ -18,6 +18,6 @@ record 相关命令索引。 - 聚合页只保留目录职责;每个命令的详细说明请进入对应单命令文档。 - 所有 `+xxx-list` 调用都必须串行执行;若要批量跑多个 list 请求,只能串行执行。 -- `+record-list / +record-get` 的字段筛选统一使用 repeatable `--field`;每次传一个字段,可重复多次传入多个字段,接受字段 ID 或字段名。 +- `+record-list / +record-get` 的字段筛选统一使用 repeatable `--field-id`;每次传一个字段,可重复多次传入多个字段,接受字段 ID 或字段名。 - 写记录 JSON 前优先阅读 [lark-base-shortcut-record-value.md](lark-base-shortcut-record-value.md)。 - 本地文件写入附件字段时,必须使用 `+record-upload-attachment`。 From f74fe3a3b7e24f6f75d592a571ecbc0793ba8203 Mon Sep 17 00:00:00 2001 From: kongenpei Date: Wed, 8 Apr 2026 20:29:19 +0800 Subject: [PATCH 4/5] docs(base): clarify record-list field_scope priority --- skills/lark-base/references/lark-base-record-list.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skills/lark-base/references/lark-base-record-list.md b/skills/lark-base/references/lark-base-record-list.md index fa3866925..f785a85f9 100644 --- a/skills/lark-base/references/lark-base-record-list.md +++ b/skills/lark-base/references/lark-base-record-list.md @@ -9,6 +9,12 @@ | 字段 | 类型 | 说明 | |------|------|------| | `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)。 ## 按需翻页规则 From 7c69b7ad010496097afeb97846e22eff58cab245 Mon Sep 17 00:00:00 2001 From: kongenpei Date: Thu, 9 Apr 2026 00:22:39 +0800 Subject: [PATCH 5/5] refactor(base): remove field-id from record-get --- shortcuts/base/base_dryrun_ops_test.go | 5 +-- shortcuts/base/base_execute_test.go | 44 ------------------- shortcuts/base/base_shortcuts_test.go | 2 +- shortcuts/base/record_get.go | 1 - shortcuts/base/record_ops.go | 22 +++------- skills/lark-base/SKILL.md | 2 +- .../references/lark-base-record-get.md | 9 ---- .../lark-base/references/lark-base-record.md | 2 +- 8 files changed, 10 insertions(+), 77 deletions(-) diff --git a/shortcuts/base/base_dryrun_ops_test.go b/shortcuts/base/base_dryrun_ops_test.go index 8e13cf302..80ca46299 100644 --- a/shortcuts/base/base_dryrun_ops_test.go +++ b/shortcuts/base/base_dryrun_ops_test.go @@ -85,13 +85,12 @@ func TestDryRunRecordOps(t *testing.T) { ) assertDryRunContains(t, dryRunRecordUpsert(ctx, upsertCreateRT), "POST /open-apis/base/v3/bases/app_x/tables/tbl_1/records") - rt := newBaseTestRuntimeWithArrays( + rt := newBaseTestRuntime( map[string]string{"base-token": "app_x", "table-id": "tbl_1", "record-id": "rec_1", "json": `{"Name":"B"}`}, - map[string][]string{"field-id": {"Name", "Age"}}, nil, map[string]int{"max-version": 11, "page-size": 30}, ) - assertDryRunContains(t, dryRunRecordGet(ctx, rt), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/records/rec_1", "field=Name", "field=Age") + assertDryRunContains(t, dryRunRecordGet(ctx, rt), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1/records/rec_1") assertDryRunContains(t, dryRunRecordUpsert(ctx, rt), "PATCH /open-apis/base/v3/bases/app_x/tables/tbl_1/records/rec_1") assertDryRunContains(t, dryRunRecordDelete(ctx, rt), "DELETE /open-apis/base/v3/bases/app_x/tables/tbl_1/records/rec_1") assertDryRunContains(t, dryRunRecordHistoryList(ctx, rt), "GET /open-apis/base/v3/bases/app_x/record_history", "max_version=11", "page_size=30", "record_id=rec_1", "table_id=tbl_1") diff --git a/shortcuts/base/base_execute_test.go b/shortcuts/base/base_execute_test.go index b8e5e937e..78dca55d2 100644 --- a/shortcuts/base/base_execute_test.go +++ b/shortcuts/base/base_execute_test.go @@ -578,50 +578,6 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) { } }) - t.Run("get with 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_x/records/rec_fields?field=Name&field=Age", - 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}}, - }, - }, - }) - if err := runShortcut(t, BaseRecordGet, []string{"+record-get", "--base-token", "app_x", "--table-id", "tbl_x", "--record-id", "rec_fields", "--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("get with comma field", 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/rec_comma?field=A%2CB", - Body: map[string]interface{}{ - "code": 0, - "data": map[string]interface{}{ - "fields": []interface{}{"A,B"}, - "record_id_list": []interface{}{"rec_comma"}, - "data": []interface{}{[]interface{}{"value-1"}}, - }, - }, - }) - if err := runShortcut(t, BaseRecordGet, []string{"+record-get", "--base-token", "app_x", "--table-id", "tbl_x", "--record-id", "rec_comma", "--field-id", "A,B"}, factory, stdout); err != nil { - t.Fatalf("err=%v", err) - } - if got := stdout.String(); !strings.Contains(got, `"A,B"`) || !strings.Contains(got, `"rec_comma"`) { - t.Fatalf("stdout=%s", got) - } - }) - t.Run("get passthrough fallback", 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 a8bdc5da6..13cb2df25 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -251,7 +251,7 @@ func TestBaseRecordValidate(t *testing.T) { t.Fatalf("record list validate should be nil for repeatable --field-id") } if BaseRecordGet.Validate != nil { - t.Fatalf("record get validate should be nil for repeatable --field-id") + 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/record_get.go b/shortcuts/base/record_get.go index e56868acb..b54c5466d 100644 --- a/shortcuts/base/record_get.go +++ b/shortcuts/base/record_get.go @@ -20,7 +20,6 @@ var BaseRecordGet = common.Shortcut{ baseTokenFlag(true), tableRefFlag(true), recordRefFlag(true), - {Name: "field-id", Type: "string_array", Desc: "field ID or field name to include (repeatable)"}, }, DryRun: dryRunRecordGet, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/base/record_ops.go b/shortcuts/base/record_ops.go index 8e4765caa..d6407625c 100644 --- a/shortcuts/base/record_ops.go +++ b/shortcuts/base/record_ops.go @@ -20,7 +20,7 @@ func dryRunRecordList(_ context.Context, runtime *common.RuntimeContext) *common params := url.Values{} params.Set("offset", strconv.Itoa(offset)) params.Set("limit", strconv.Itoa(limit)) - for _, field := range recordFields(runtime) { + for _, field := range recordListFields(runtime) { params.Add("field_id", field) } if viewID := runtime.Str("view-id"); viewID != "" { @@ -34,16 +34,8 @@ func dryRunRecordList(_ context.Context, runtime *common.RuntimeContext) *common } func dryRunRecordGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { - path := "/open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id" - if fields := recordFields(runtime); len(fields) > 0 { - params := url.Values{} - for _, field := range fields { - params.Add("field", field) - } - path += "?" + params.Encode() - } return common.NewDryRunAPI(). - GET(path). + GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id"). Set("base_token", runtime.Str("base-token")). Set("table_id", baseTableID(runtime)). Set("record_id", runtime.Str("record-id")) @@ -93,7 +85,7 @@ func validateRecordJSON(runtime *common.RuntimeContext) error { return nil } -func recordFields(runtime *common.RuntimeContext) []string { +func recordListFields(runtime *common.RuntimeContext) []string { return runtime.StrArray("field-id") } @@ -104,7 +96,7 @@ func executeRecordList(runtime *common.RuntimeContext) error { } limit := common.ParseIntBounded(runtime, "limit", 1, 200) params := map[string]interface{}{"offset": offset, "limit": limit} - fields := recordFields(runtime) + fields := recordListFields(runtime) if len(fields) > 0 { params["field_id"] = fields } @@ -120,11 +112,7 @@ func executeRecordList(runtime *common.RuntimeContext) error { } func executeRecordGet(runtime *common.RuntimeContext) error { - params := map[string]interface{}{} - if fields := recordFields(runtime); len(fields) > 0 { - params["field"] = fields - } - data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "records", runtime.Str("record-id")), params, nil) + data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "records", runtime.Str("record-id")), nil, nil) if err != nil { return err } diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index 92ef1720c..b431dc06a 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -156,7 +156,7 @@ 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**:`--limit` 最大 `200`。先拉首批并检查 `has_more`;仅当 `has_more=true` 且用户明确需要更多数据(如“全部导出/全量明细/继续下一页”)时再按 `offset` 递增翻页,禁止单次传超过 `200` -- **记录读取字段筛选**:`+record-list / +record-get` 统一使用 repeatable `--field-id`;每次传一个字段,接受字段 ID 或字段名,例如 `--field-id fld_status --field-id 项目名称`;不指定字段时默认返回所有字段 +- **记录读取字段筛选**:`+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 396a69d13..6499126bd 100644 --- a/skills/lark-base/references/lark-base-record-get.md +++ b/skills/lark-base/references/lark-base-record-get.md @@ -11,13 +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 \ - --field-id fld_status \ - --field-id 项目名称 ``` ## 参数 @@ -27,7 +20,6 @@ lark-cli base +record-get \ | `--base-token ` | 是 | Base Token | | `--table-id ` | 是 | 表 ID 或表名 | | `--record-id ` | 是 | 记录 ID | -| `--field-id ` | 否 | 字段 ID 或字段名;可重复传入多个 `--field-id` 裁剪返回列;不指定时默认返回所有字段 | ## API 入参详情 @@ -40,7 +32,6 @@ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id ## 返回重点 - 成功时直接返回接口 `data` 字段内容。 -- 传了 `--field-id` 时,由服务端按字段裁剪返回结果。 ## 参考 diff --git a/skills/lark-base/references/lark-base-record.md b/skills/lark-base/references/lark-base-record.md index 10aeacb7c..1b3e59f27 100644 --- a/skills/lark-base/references/lark-base-record.md +++ b/skills/lark-base/references/lark-base-record.md @@ -18,6 +18,6 @@ record 相关命令索引。 - 聚合页只保留目录职责;每个命令的详细说明请进入对应单命令文档。 - 所有 `+xxx-list` 调用都必须串行执行;若要批量跑多个 list 请求,只能串行执行。 -- `+record-list / +record-get` 的字段筛选统一使用 repeatable `--field-id`;每次传一个字段,可重复多次传入多个字段,接受字段 ID 或字段名。 +- `+record-list` 支持重复传参 `--field-id` 做字段筛选。 - 写记录 JSON 前优先阅读 [lark-base-shortcut-record-value.md](lark-base-shortcut-record-value.md)。 - 本地文件写入附件字段时,必须使用 `+record-upload-attachment`。