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
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 @@ -104,6 +104,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, dryRunRecordBatchCreate(ctx, upsertCreateRT), "POST /open-apis/base/v3/bases/app_x/tables/tbl_1/records/batch_create")
assertDryRunContains(t, dryRunRecordBatchUpdate(ctx, upsertCreateRT), "POST /open-apis/base/v3/bases/app_x/tables/tbl_1/records/batch_update")

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

t.Run("batch create", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/batch_create",
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, BaseRecordBatchCreate, []string{"+record-batch-create", "--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 update", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/batch_update",
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, BaseRecordBatchUpdate, []string{"+record-batch-update", "--base-token", "app_x", "--table-id", "tbl_x", "--json", `{"record_id_list":["rec_1"],"patch":{"Status":"Done"}}`}, 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 update passthrough", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
updateStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/batch_update",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"record_id_list": []interface{}{"rec_1"},
},
},
}
reg.Register(updateStub)
if err := runShortcut(t, BaseRecordBatchUpdate, []string{"+record-batch-update", "--base-token", "app_x", "--table-id", "tbl_x", "--json", `{"record_id_list":["rec_1"],"patch":{"Name":"Alice","Status":"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, `"record_id_list":["rec_1"]`) || !strings.Contains(body, `"patch":{"Name":"Alice","Status":"Done"}`) {
t.Fatalf("request body=%s", body)
}
})

t.Run("delete", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Expand Down
3 changes: 1 addition & 2 deletions shortcuts/base/base_shortcuts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,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-visible-fields", "+view-set-visible-fields", "+view-get-group", "+view-set-group", "+view-get-sort", "+view-set-sort", "+view-get-timebar", "+view-set-timebar", "+view-get-card", "+view-set-card", "+view-rename",
"+record-list", "+record-search", "+record-get", "+record-upsert", "+record-upload-attachment", "+record-delete",
"+record-list", "+record-search", "+record-get", "+record-upsert", "+record-batch-create", "+record-batch-update", "+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 Expand Up @@ -265,7 +265,6 @@ func TestBaseRecordValidate(t *testing.T) {
t.Fatalf("record upsert validate should be nil for API passthrough")
}
}

func TestBaseViewValidate(t *testing.T) {
ctx := context.Background()
if err := BaseViewCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "tbl_1", "json": `{"name":"Main"}`}, nil, nil)); err != nil {
Expand Down
32 changes: 32 additions & 0 deletions shortcuts/base/record_batch_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package base

import (
"context"

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

var BaseRecordBatchCreate = common.Shortcut{
Service: "base",
Command: "+record-batch-create",
Description: "Batch create records",
Risk: "write",
Scopes: []string{"base:record:create"},
AuthTypes: authTypes(),
Flags: []common.Flag{
baseTokenFlag(true),
tableRefFlag(true),
{Name: "json", Desc: "batch create JSON object", Required: true},
},
Tips: []string{
`Example: --json '{"fields":["Title","Status"],"rows":[["Task A","Open"],["Task B","Done"]]}'`,
"Agent hint: use the lark-base skill's record-batch-create guide for usage and limits.",
},
DryRun: dryRunRecordBatchCreate,
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeRecordBatchCreate(runtime)
},
}
32 changes: 32 additions & 0 deletions shortcuts/base/record_batch_update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package base

import (
"context"

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

var BaseRecordBatchUpdate = common.Shortcut{
Service: "base",
Command: "+record-batch-update",
Description: "Batch update records",
Risk: "write",
Scopes: []string{"base:record:update"},
AuthTypes: authTypes(),
Flags: []common.Flag{
baseTokenFlag(true),
tableRefFlag(true),
{Name: "json", Desc: "batch update JSON object", Required: true},
},
Tips: []string{
`Example: --json '{"record_id_list":["recXXX"],"patch":{"Status":"Done"}}'`,
"Agent hint: use the lark-base skill's record-batch-update guide for usage and limits.",
},
DryRun: dryRunRecordBatchUpdate,
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeRecordBatchUpdate(runtime)
},
}
51 changes: 51 additions & 0 deletions shortcuts/base/record_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,26 @@ func dryRunRecordUpsert(_ context.Context, runtime *common.RuntimeContext) *comm
Set("table_id", baseTableID(runtime))
}

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

func dryRunRecordBatchUpdate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
pc := newParseCtx(runtime)
body, _ := parseJSONObject(pc, runtime.Str("json"), "json")
return common.NewDryRunAPI().
POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/records/batch_update").
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 @@ -99,6 +119,7 @@ func validateRecordJSON(runtime *common.RuntimeContext) error {
func recordListFields(runtime *common.RuntimeContext) []string {
return runtime.StrArray("field-id")
}

func executeRecordList(runtime *common.RuntimeContext) error {
offset := runtime.Int("offset")
if offset < 0 {
Expand Down Expand Up @@ -168,6 +189,36 @@ func executeRecordUpsert(runtime *common.RuntimeContext) error {
return nil
}

func executeRecordBatchCreate(runtime *common.RuntimeContext) error {
pc := newParseCtx(runtime)
body, err := parseJSONObject(pc, 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_create"), nil, body)
data, err := handleBaseAPIResult(result, err, "batch create records")
if err != nil {
return err
}
runtime.Out(data, nil)
return nil
}

func executeRecordBatchUpdate(runtime *common.RuntimeContext) error {
pc := newParseCtx(runtime)
body, err := parseJSONObject(pc, 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_update"), nil, body)
data, err := handleBaseAPIResult(result, err, "batch update 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 @@ -40,6 +40,8 @@ func Shortcuts() []common.Shortcut {
BaseRecordSearch,
BaseRecordGet,
BaseRecordUpsert,
BaseRecordBatchCreate,
BaseRecordBatchUpdate,
BaseRecordUploadAttachment,
BaseRecordDelete,
BaseRecordHistoryList,
Expand Down
19 changes: 12 additions & 7 deletions skills/lark-base/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,18 @@ metadata:
- 不要把 `+record-list / +record-search` 当聚合分析引擎
- 不要没读 guide 就直接创建 formula / lookup 字段
- 不要凭自然语言猜表名、字段名、公式表达式里的字段引用
- 不要把系统字段、formula 字段、lookup 字段当成 `+record-upsert` 的写入目标
- 不要把系统字段、formula 字段、lookup 字段当成 `+record-upsert / +record-batch-create / +record-batch-update` 的写入目标
- 不要把“本地 Excel / CSV 导入成 Base”误判成 `+base-create`、`+table-create` 或 `+record-upsert`;这一步必须先走 `lark-cli drive +import --type bitable`
- 不要在 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-create / +record-batch-update` 写入,例如文本、数字、日期、单选、多选、人员、关联。**附件字段例外**:对 agent 而言,文件上传必须走 `+record-upload-attachment`。
- **系统字段**:平台自动维护,只读,典型包括创建时间、最后更新时间、创建人、修改人、自动编号。
- **计算字段**:通过表达式或跨表规则推导,只读,典型包括 **公式字段(formula)** 和 **查找引用字段(lookup)**。
2. **写记录前先判断字段类别** — 只有存储字段可直接写;公式 / lookup / 创建时间 / 更新时间 / 创建人 / 修改人 / 自动编号都应视为只读输出字段,不能拿来做 `+record-upsert` 入参。
2. **写记录前先判断字段类别** — 只有存储字段可直接写;公式 / lookup / 创建时间 / 更新时间 / 创建人 / 修改人 / 自动编号都应视为只读输出字段,不能拿来做 `+record-upsert / +record-batch-create / +record-batch-update` 入参。
3. **Base 不只是存表数据,也能内建计算** — 用户提出“统计、比较、排名、文本拼接、日期差、跨表汇总、状态判断”等需求时,不能默认导出数据后手算;要先判断是否应通过 `+data-query` 或公式字段在 Base 内完成。

## 分析路径决策
Expand Down Expand Up @@ -109,7 +109,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-create / +record-batch-update / +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)、[lark-base-record-search.md](references/lark-base-record-search.md);默认优先 `+record-list`,仅当用户提供明确搜索关键词时使用 `+record-search`;视图筛选读取用 `+view-set-filter` + `+record-list`
Expand Down Expand Up @@ -143,6 +143,8 @@ metadata:
| 关键词搜索记录 | `lark-cli base +record-search` | 透传搜索参数;用于关键词检索,不用于聚合分析 |
| 列表 / 获取记录 | `lark-cli base +record-list` / `+record-get` | 原子命令,如果需要`聚合计算`,`分组统计` 推荐走 `+data-query` |
| 创建 / 更新记录 | `lark-cli base +record-upsert` | `--table-id [--record-id] --json` |
| 批量创建记录 | `lark-cli base +record-batch-create` | 适合大量创建写入(如 CSV/Excel 导入);先按 record-value 规范整理为 `json.fields + json.rows` |
| 批量更新指定记录 | `lark-cli base +record-batch-update` | `--table-id --json`(`json.record_id_list + json.patch`) |
| 聚合分析 / 比较排序 / 求最值 / 筛选统计 | `lark-cli base +data-query` | 不要用 `+record-list / +record-search` 拉全量数据再手动计算,需使用 `+data-query` 走服务端计算 |
| 配置 / 查询视图 | `lark-cli base +view-*` | `list/get/create/delete/get-*/set-*/rename` |
| 查看记录历史 | `lark-cli base +record-history-list` | 按表和记录查询变更历史 |
Expand All @@ -169,6 +171,7 @@ metadata:
- **记录读取字段筛选**:`+record-list` 支持重复传参 `--field-id` 做字段筛选
- **`+record-list / +record-search` 选择规则**:优先使用 `+record-list`;仅当用户给出明确搜索关键词时,才使用 `+record-search`
- **`+record-search` 使用规则**:仅通过 `--json` 传搜索请求体;`keyword/search_fields/offset/limit` 等字段合法性由 API 侧按 schema 校验
- **`+record-batch-create` 适用边界**:优先用于大量创建写入(如 CSV/Excel 导入)。当输入是长表格或长文本时,先按 [lark-base-shortcut-record-value.md](references/lark-base-shortcut-record-value.md) 做字段映射和类型规范化,再组装 `fields + rows` 调用命令写入
- **字段可写性先判断**:存储字段才可写;公式 / lookup / 系统字段默认只读,写记录时应跳过
- **公式能力要主动想到**:用户说“算一下”“生成标签”“判断是否异常”“跨表汇总”“按日期差预警”时,要先判断是否应该建公式字段,而不是只返回手工分析方案
- **lookup 不是默认首选**:lookup 只在用户明确要求或确实更适合固定查找模型时使用;常规计算、跨表聚合和条件判断优先 formula
Expand Down Expand Up @@ -277,9 +280,11 @@ 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 规范(推荐)
- [lark-base-shortcut-field-properties.md](references/lark-base-shortcut-field-properties.md) — `+field-create/+field-update` 调用前必看,各类型 field 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-shortcut-record-value.md](references/lark-base-shortcut-record-value.md) — record 写入(`+record-upsert / +record-batch-create / +record-batch-update`)调用前必看,各类型 record JSON 规范
- [lark-base-record-batch-create.md](references/lark-base-record-batch-create.md) — `+record-batch-create` 用法与 `--json` 结构
- [lark-base-record-batch-update.md](references/lark-base-record-batch-update.md) — `+record-batch-update` 用法与 `--json` 结构
- [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 Down Expand Up @@ -307,7 +312,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-search / +record-list / +record-get / +record-upsert / +record-upload-attachment / +record-delete` |
| [`record commands`](references/lark-base-record.md) | `+record-search / +record-list / +record-get / +record-upsert / +record-batch-create / +record-batch-update / +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
Loading
Loading