Skip to content
Draft
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
2 changes: 2 additions & 0 deletions shortcuts/base/base_dryrun_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ func TestDryRunRecordOps(t *testing.T) {
nil, nil,
)
assertDryRunContains(t, dryRunRecordUpsert(ctx, upsertCreateRT), "POST /open-apis/base/v3/bases/app_x/tables/tbl_1/records")
assertDryRunContains(t, dryRunRecordBatchAdd(ctx, upsertCreateRT), "POST /open-apis/base/v3/bases/app_x/tables/tbl_1/records/batch")
assertDryRunContains(t, dryRunRecordBatchSet(ctx, upsertCreateRT), "PATCH /open-apis/base/v3/bases/app_x/tables/tbl_1/records/batch")

rt := newBaseTestRuntime(
map[string]string{"base-token": "app_x", "table-id": "tbl_1", "record-id": "rec_1", "json": `{"Name":"B"}`},
Expand Down
72 changes: 72 additions & 0 deletions shortcuts/base/base_execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,78 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) {
}
})

t.Run("batch add", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/batch",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"fields": []interface{}{"Name"},
"record_id_list": []interface{}{"rec_1", "rec_2"},
"data": []interface{}{[]interface{}{"Alice"}, []interface{}{"Bob"}},
},
},
})
if err := runShortcut(t, BaseRecordBatchAdd, []string{"+record-batch-add", "--base-token", "app_x", "--table-id", "tbl_x", "--json", `{"fields":["Name"],"rows":[["Alice"],["Bob"]]}`}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
if got := stdout.String(); !strings.Contains(got, `"record_id_list"`) || !strings.Contains(got, `"rec_1"`) || !strings.Contains(got, `"Alice"`) {
t.Fatalf("stdout=%s", got)
}
})

t.Run("batch set", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/batch",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"has_more": false,
"record_id_list": []interface{}{"rec_1"},
"update": map[string]interface{}{"Status": "Done"},
},
},
})
if err := runShortcut(t, BaseRecordBatchSet, []string{"+record-batch-set", "--base-token", "app_x", "--table-id", "tbl_x", "--json", `{"patch":{"Status":"Done"},"filter":{"logic":"and","conditions":[["Status","is","Open"]]},"offset":0,"limit":200}`}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
if got := stdout.String(); !strings.Contains(got, `"record_id_list"`) || !strings.Contains(got, `"update"`) || !strings.Contains(got, `"Done"`) {
t.Fatalf("stdout=%s", got)
}
})

t.Run("batch set matrix passthrough", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
updateStub := &httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/batch",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"record_id_list": []interface{}{"rec_1"},
},
},
}
reg.Register(updateStub)
if err := runShortcut(t, BaseRecordBatchSet, []string{"+record-batch-set", "--base-token", "app_x", "--table-id", "tbl_x", "--json", `{"records":["rec_1"],"fields":["fld_name","fld_status"],"cells":[["Alice","Done"]]}`}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
if got := stdout.String(); !strings.Contains(got, `"record_id_list"`) || !strings.Contains(got, `"rec_1"`) {
t.Fatalf("stdout=%s", got)
}
body := string(updateStub.CapturedBody)
if !strings.Contains(body, `"records":["rec_1"]`) || !strings.Contains(body, `"fields":["fld_name","fld_status"]`) || !strings.Contains(body, `"cells":[["Alice","Done"]]`) {
t.Fatalf("request body=%s", body)
}
})

t.Run("delete", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
Expand Down
2 changes: 1 addition & 1 deletion shortcuts/base/base_shortcuts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func TestShortcutsCatalog(t *testing.T) {
"+table-list", "+table-get", "+table-create", "+table-update", "+table-delete",
"+field-list", "+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-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-get", "+record-upsert", "+record-upload-attachment", "+record-delete",
"+record-list", "+record-get", "+record-upsert", "+record-batch-add", "+record-batch-set", "+record-upload-attachment", "+record-delete",
"+record-history-list",
"+base-get", "+base-copy", "+base-create",
"+role-create", "+role-delete", "+role-update", "+role-list", "+role-get", "+advperm-enable", "+advperm-disable",
Expand Down
31 changes: 31 additions & 0 deletions shortcuts/base/record_batch_add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package base

import (
"context"

"github.com/larksuite/cli/shortcuts/common"
)

var BaseRecordBatchAdd = common.Shortcut{
Service: "base",
Command: "+record-batch-add",
Description: "Batch add records",
Risk: "write",
Scopes: []string{"base:record:create"},
AuthTypes: authTypes(),
Flags: []common.Flag{
baseTokenFlag(true),
tableRefFlag(true),
{Name: "json", Desc: "batch add JSON object, e.g. {\"fields\":[],\"rows\":[]}", Required: true},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
return validateRecordJSON(runtime)
},
DryRun: dryRunRecordBatchAdd,
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeRecordBatchAdd(runtime)
},
}
31 changes: 31 additions & 0 deletions shortcuts/base/record_batch_set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package base

import (
"context"

"github.com/larksuite/cli/shortcuts/common"
)

var BaseRecordBatchSet = common.Shortcut{
Service: "base",
Command: "+record-batch-set",
Description: "Batch set records",
Risk: "write",
Scopes: []string{"base:record:update"},
AuthTypes: authTypes(),
Flags: []common.Flag{
baseTokenFlag(true),
tableRefFlag(true),
{Name: "json", Desc: "batch set JSON object, passed through as request body, e.g. {\"patch\":{},\"filter\":{},\"offset\":0,\"limit\":200} or {\"records\":[],\"fields\":[],\"cells\":[]}", Required: true},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
return validateRecordJSON(runtime)
},
DryRun: dryRunRecordBatchSet,
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeRecordBatchSet(runtime)
},
}
46 changes: 46 additions & 0 deletions shortcuts/base/record_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ func dryRunRecordUpsert(_ context.Context, runtime *common.RuntimeContext) *comm
Set("table_id", baseTableID(runtime))
}

func dryRunRecordBatchAdd(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
body, _ := parseJSONObject(runtime.Str("json"), "json")
return common.NewDryRunAPI().
POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/batch").
Body(body).
Set("base_token", runtime.Str("base-token")).
Set("table_id", baseTableID(runtime))
}

func dryRunRecordBatchSet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
body, _ := parseJSONObject(runtime.Str("json"), "json")
return common.NewDryRunAPI().
PATCH("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/batch").
Body(body).
Set("base_token", runtime.Str("base-token")).
Set("table_id", baseTableID(runtime))
}

func dryRunRecordDelete(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
return common.NewDryRunAPI().
DELETE("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/:record_id").
Expand Down Expand Up @@ -128,6 +146,34 @@ func executeRecordUpsert(runtime *common.RuntimeContext) error {
return nil
}

func executeRecordBatchAdd(runtime *common.RuntimeContext) error {
body, err := parseJSONObject(runtime.Str("json"), "json")
if err != nil {
return err
}
result, err := baseV3Raw(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "records", "batch"), nil, body)
data, err := handleBaseAPIResult(result, err, "batch add records")
if err != nil {
return err
}
runtime.Out(data, nil)
return nil
}

func executeRecordBatchSet(runtime *common.RuntimeContext) error {
body, err := parseJSONObject(runtime.Str("json"), "json")
if err != nil {
return err
}
result, err := baseV3Raw(runtime, "PATCH", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "records", "batch"), nil, body)
data, err := handleBaseAPIResult(result, err, "batch set records")
if err != nil {
return err
}
runtime.Out(data, nil)
return nil
}

func executeRecordDelete(runtime *common.RuntimeContext) error {
_, err := baseV3Call(runtime, "DELETE", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "records", runtime.Str("record-id")), nil, nil)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions shortcuts/base/shortcuts.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func Shortcuts() []common.Shortcut {
BaseRecordList,
BaseRecordGet,
BaseRecordUpsert,
BaseRecordBatchAdd,
BaseRecordBatchSet,
BaseRecordUploadAttachment,
BaseRecordDelete,
BaseRecordHistoryList,
Expand Down
14 changes: 9 additions & 5 deletions skills/lark-base/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ metadata:
- 不要把 `+record-list` 当聚合分析引擎
- 不要没读 guide 就直接创建 formula / lookup 字段
- 不要凭自然语言猜表名、字段名、公式表达式里的字段引用
- 不要把系统字段、formula 字段、lookup 字段当成 `+record-upsert` 的写入目标
- 不要把系统字段、formula 字段、lookup 字段当成 `+record-upsert / +record-batch-add / +record-batch-set` 的写入目标
- 不要在 Base 场景改走 `lark-cli api GET /open-apis/bitable/v1/...`
- 不要因为 wiki 解析结果里的 `obj_type=bitable` 就去找 `bitable.*`;在本 CLI 里应继续使用 `lark-cli base +...`

## Base 基本心智模型

1. **Base 字段分三类**
- **存储字段**:真实存用户输入的数据,通常适合 `+record-upsert` 写入,例如文本、数字、日期、单选、多选、人员、关联。**附件字段例外**:对 agent 而言,文件上传必须走 `+record-upload-attachment`。
- **存储字段**:真实存用户输入的数据,通常适合 `+record-upsert / +record-batch-add / +record-batch-set` 写入,例如文本、数字、日期、单选、多选、人员、关联。**附件字段例外**:对 agent 而言,文件上传必须走 `+record-upload-attachment`。
- **系统字段**:平台自动维护,只读,典型包括创建时间、最后更新时间、创建人、修改人、自动编号。
- **计算字段**:通过表达式或跨表规则推导,只读,典型包括 **公式字段(formula)** 和 **查找引用字段(lookup)**。
2. **写记录前先判断字段类别** — 只有存储字段可直接写;公式 / lookup / 创建时间 / 更新时间 / 创建人 / 修改人 / 自动编号都应视为只读输出字段,不能拿来做 `+record-upsert` 入参。
2. **写记录前先判断字段类别** — 只有存储字段可直接写;公式 / lookup / 创建时间 / 更新时间 / 创建人 / 修改人 / 自动编号都应视为只读输出字段,不能拿来做 `+record-upsert / +record-batch-add / +record-batch-set` 入参。
3. **Base 不只是存表数据,也能内建计算** — 用户提出“统计、比较、排名、文本拼接、日期差、跨表汇总、状态判断”等需求时,不能默认导出数据后手算;要先判断是否应通过 `+data-query` 或公式字段在 Base 内完成。

## 分析路径决策
Expand Down Expand Up @@ -101,7 +101,7 @@ metadata:

## 核心规则

1. **只使用原子命令** — 使用 `+table-list / +table-get / +field-create / +record-upsert / +view-set-filter / +record-history-list / +base-get` 这类一命令一动作的写法,不使用旧聚合式 `+table / +field / +record / +view / +history / +workspace`
1. **只使用原子命令** — 使用 `+table-list / +table-get / +field-create / +record-upsert / +record-batch-add / +record-batch-set / +view-set-filter / +record-history-list / +base-get` 这类一命令一动作的写法,不使用旧聚合式 `+table / +field / +record / +view / +history / +workspace`
2. **写记录前先读字段结构** — 先调用 `+field-list` 获取字段结构,再读 [lark-base-shortcut-record-value.md](references/lark-base-shortcut-record-value.md) 确认各字段类型的写入值格式
3. **写字段前先看字段属性规范** — 先读 [lark-base-shortcut-field-properties.md](references/lark-base-shortcut-field-properties.md) 确认 `+field-create/+field-update` 的 JSON 结构
4. **筛选查询按视图能力执行** — 先读 [lark-base-view-set-filter.md](references/lark-base-view-set-filter.md) 和 [lark-base-record-list.md](references/lark-base-record-list.md),通过 `+view-set-filter` + `+record-list` 组合完成筛选读取
Expand Down Expand Up @@ -134,6 +134,8 @@ metadata:
| 创建 / 更新 lookup 字段 | `lark-cli base +field-create` / `+field-update` | `type=lookup`;先读 lookup guide,再创建 / 更新,默认先判断 formula 是否更合适 |
| 列表 / 获取记录 | `lark-cli base +record-list` / `+record-get` | 原子命令,如果需要`聚合计算`,`分组统计` 推荐走 `+data-query` |
| 创建 / 更新记录 | `lark-cli base +record-upsert` | `--table-id [--record-id] --json` |
| 批量新增记录 | `lark-cli base +record-batch-add` | `--table-id --json`(`json.fields + json.rows`) |
| 按条件批量更新记录 | `lark-cli base +record-batch-set` | `--table-id --json`(`json.patch + json.filter + json.offset + json.limit`) |
| 聚合分析 / 比较排序 / 求最值 / 筛选统计 | `lark-cli base +data-query` | 不要用 `+record-list` 拉全量数据再手动计算,需使用 `+data-query` 走服务端计算 |
| 配置 / 查询视图 | `lark-cli base +view-*` | `list/get/create/delete/get-*/set-*/rename` |
| 查看记录历史 | `lark-cli base +record-history-list` | 按表和记录查询变更历史 |
Expand Down Expand Up @@ -267,6 +269,8 @@ https://{domain}/base/{base-token}?table={table-id}&view={view-id}
- [lark-base-shortcut-field-properties.md](references/lark-base-shortcut-field-properties.md) — `+field-create/+field-update` JSON 规范(推荐)
- [role-config.md](references/role-config.md) — 角色权限配置详解
- [lark-base-shortcut-record-value.md](references/lark-base-shortcut-record-value.md) — `+record-upsert` 值格式规范(推荐)
- [lark-base-record-batch-add.md](references/lark-base-record-batch-add.md) — `+record-batch-add` JSON 结构与 raw schema
- [lark-base-record-batch-set.md](references/lark-base-record-batch-set.md) — `+record-batch-set` JSON 结构与 raw schema
- [formula-field-guide.md](references/formula-field-guide.md) — formula 字段写法、函数约束、CurrentValue 规则、跨表计算模式(强烈推荐)
- [lookup-field-guide.md](references/lookup-field-guide.md) — lookup 字段配置规则、where/aggregate 约束、与 formula 的取舍
- [lark-base-view-set-filter.md](references/lark-base-view-set-filter.md) — 视图筛选配置
Expand All @@ -293,7 +297,7 @@ https://{domain}/base/{base-token}?table={table-id}&view={view-id}
|----------|------|
| [`table commands`](references/lark-base-table.md) | `+table-list / +table-get / +table-create / +table-update / +table-delete` |
| [`field commands`](references/lark-base-field.md) | `+field-list / +field-get / +field-create / +field-update / +field-delete / +field-search-options` |
| [`record commands`](references/lark-base-record.md) | `+record-list / +record-get / +record-upsert / +record-upload-attachment / +record-delete` |
| [`record commands`](references/lark-base-record.md) | `+record-list / +record-get / +record-upsert / +record-batch-add / +record-batch-set / +record-upload-attachment / +record-delete` |
| [`view commands`](references/lark-base-view.md) | `+view-list / +view-get / +view-create / +view-delete / +view-get-* / +view-set-* / +view-rename` |
| [`data-query commands`](references/lark-base-data-query.md) | `+data-query` |
| [`history commands`](references/lark-base-history.md) | `+record-history-list` |
Expand Down
55 changes: 55 additions & 0 deletions skills/lark-base/references/lark-base-record-batch-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# base +record-batch-add

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

批量新增记录。

## 推荐命令

```bash
lark-cli base +record-batch-add \
--base-token app_xxx \
--table-id tbl_xxx \
--json '{"fields":["标题","状态"],"rows":[["任务 A","Open"],["任务 B","Done"]]}'
```

## 参数

| 参数 | 必填 | 说明 |
|------|------|------|
| `--base-token <token>` | 是 | Base Token |
| `--table-id <id_or_name>` | 是 | 表 ID 或表名 |
| `--json <body>` | 是 | 批量新增请求体,必须是 JSON 对象 |

## API 入参详情

**HTTP 方法和路径:**

```
POST /open-apis/base/v3/bases/:base_token/tables/:table_id/records/batch
```

## `--json` Raw JSON Schema

```json
{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","minLength":1,"maxLength":100,"description":"Field id or name"},"minItems":1,"maxItems":200},"rows":{"type":"array","items":{"type":"array","items":{"anyOf":[{"anyOf":[{"type":"string","description":"text field cell, example: \"one string and [one url](https://foo.bar)\""},{"type":"number","description":"number field cell, can be any float64 value"},{"type":"array","items":{"type":"string","description":"option name"},"description":"select field cell, example: [\"option_1\", \"option_2\"]"},{"type":"string","description":"datetime field cell. accepts common datetime strings and timestamp-like values. Prefer \"YYYY-MM-DD HH:mm:ss\" in requests because it is the most stable format and matches the API output. Example: \"2026-01-01 19:30:00\""},{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"record id"}},"required":["id"],"additionalProperties":false},"description":"link field cell, example: [{\"id\": \"rec_123\"}]"},{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"user id"}},"required":["id"],"additionalProperties":false},"description":"user field cell, example: [{\"id\": \"ou_123\"}]"},{"type":"object","properties":{"lng":{"type":"number","description":"Longitude"},"lat":{"type":"number","description":"Latitude"}},"required":["lng","lat"],"additionalProperties":false,"description":"location field cell, example: {\"lng\": 113.94765, \"lat\": 22.528533}"},{"type":"boolean","description":"checkbox field cell"},{"type":"array","items":{"type":"object","properties":{"file_token":{"type":"string","minLength":0,"maxLength":50},"name":{"type":"string","minLength":1,"maxLength":255},"mime_type":{"type":"string","maxLength":255,"description":"deprecated field"},"size":{"type":"integer","minimum":0,"description":"deprecated field"},"image_width":{"type":"integer","minimum":0,"description":"deprecated field"},"image_height":{"type":"integer","minimum":0,"description":"deprecated field"},"deprecated_set_attachment":{"type":"boolean","description":"deprecated field"}},"required":["file_token","name"],"additionalProperties":false},"description":"attachment field cell. temporary compatibility for attachment writes."},{"type":"null"}]},{"type":"null"}]}},"minItems":1,"maxItems":200}},"required":["fields","rows"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}
```

## 返回重点

- 返回对象键:
- `fields`
- `field_id_list`
- `record_id_list`
- `data`
- `ignored_fields`(可选)

## 坑点

- ⚠️ `--json` 必须是对象。
- ⚠️ `fields` 与 `rows` 列顺序必须一一对应。

## 参考

- [lark-base-record.md](lark-base-record.md) — record 索引页
- [lark-base-shortcut-record-value.md](lark-base-shortcut-record-value.md) — 记录值格式规范
Loading
Loading