Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions shortcuts/base/base_dryrun_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}`},
Expand Down
62 changes: 62 additions & 0 deletions shortcuts/base/base_execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
Expand Down
16 changes: 14 additions & 2 deletions shortcuts/base/base_shortcuts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, "")
}
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 12 additions & 1 deletion shortcuts/base/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions shortcuts/base/record_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
23 changes: 19 additions & 4 deletions shortcuts/base/record_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package base

import (
"context"
"net/url"
"strconv"

"github.com/larksuite/cli/shortcuts/common"
)
Expand All @@ -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))
}
Expand Down Expand Up @@ -78,13 +85,21 @@ 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 {
offset = 0
}
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
}
Expand Down
3 changes: 2 additions & 1 deletion skills/lark-base/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 1 addition & 9 deletions skills/lark-base/references/lark-base-record-get.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 项目名称,状态
```

## 参数
Expand All @@ -26,7 +20,6 @@ lark-cli base +record-get \
| `--base-token <token>` | 是 | Base Token |
| `--table-id <id_or_name>` | 是 | 表 ID 或表名 |
| `--record-id <id>` | 是 | 记录 ID |
| `--fields <csv_or_json>` | 否 | 字段名 CSV,或 JSON 字符串数组 |

## API 入参详情

Expand All @@ -38,8 +31,7 @@ GET /open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id

## 返回重点

- 返回 `record` 和 `raw`。
- `record` 是裁剪后的单条结果;`raw` 保留接口完整响应。
- 成功时直接返回接口 `data` 字段内容。

## 参考

Expand Down
14 changes: 12 additions & 2 deletions skills/lark-base/references/lark-base-record-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。

分页列出一张表里的记录;可按视图过滤。
分页列出一张表里的记录;可按视图过滤,也可按字段裁剪返回列
Comment thread
kongenpei marked this conversation as resolved.

## 返回关键字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `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)。

## 按需翻页规则

Expand All @@ -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
```
Expand All @@ -44,6 +52,7 @@ lark-cli base +record-list \
| `--base-token <token>` | 是 | Base Token |
| `--table-id <id_or_name>` | 是 | 表 ID 或表名 |
| `--view-id <id>` | 否 | 视图 ID;传入后只读该视图结果 |
| `--field-id <id_or_name>` | 否 | 字段 ID 或字段名;可重复传入多个 `--field-id` 裁剪返回列 |
| `--offset <n>` | 否 | 分页偏移,默认 `0` |
| `--limit <n>` | 否 | 分页大小,默认 `100`,范围 `1-200`(最大 `200`,超过会报错) |

Expand All @@ -55,14 +64,15 @@ 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`。


## 坑点

- ⚠️ `+record-list` 禁止并发调用;批量拉多个视图或多张表时必须串行。
- ⚠️ `--limit` 最大 `200`,不要传超过 `200` 的值。
- ⚠️ 分页时优先根据返回的 `has_more` 判断是否继续请求,不要盲目预拉全量数据。
- ⚠️ `--field-id` 接受字段 ID 或字段名。
- ⚠️ 复杂筛选优先落到视图里,再用 `--view-id` 读取。

## 参考
Expand Down
1 change: 1 addition & 0 deletions skills/lark-base/references/lark-base-record.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`。
Loading