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
20 changes: 20 additions & 0 deletions shortcuts/base/base_dryrun_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ func TestDryRunRecordOps(t *testing.T) {
)
assertDryRunContains(t, dryRunRecordList(ctx, commaFieldRT), "limit=1", "offset=0", "field_id=A%2CB", "field_id=C")

searchRT := newBaseTestRuntime(
map[string]string{
"base-token": "app_x",
"table-id": "tbl_1",
"json": `{"view_id":"viw_1","keyword":"Created","search_fields":["Title","fld_owner"],"select_fields":["Title","fld_owner"],"offset":-1,"limit":500}`,
},
nil, nil,
)
assertDryRunContains(
t,
dryRunRecordSearch(ctx, searchRT),
"POST /open-apis/base/v3/bases/app_x/tables/tbl_1/records/search",
`"view_id":"viw_1"`,
`"keyword":"Created"`,
`"search_fields":["Title","fld_owner"]`,
`"select_fields":["Title","fld_owner"]`,
`"offset":-1`,
`"limit":500`,
)

upsertCreateRT := newBaseTestRuntime(
map[string]string{"base-token": "app_x", "table-id": "tbl_1", "json": `{"Name":"A"}`},
nil, nil,
Expand Down
50 changes: 50 additions & 0 deletions shortcuts/base/base_execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,56 @@ func TestBaseRecordExecuteReadCreateDelete(t *testing.T) {
}
})

t.Run("search", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
searchStub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/records/search",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"fields": []interface{}{"Title", "Owner"},
"field_id_list": []interface{}{"fld_title", "fld_owner"},
"record_id_list": []interface{}{"rec_1"},
"data": []interface{}{[]interface{}{"Created by AI", "Alice"}},
"has_more": false,
"query_context": map[string]interface{}{
"record_scope": "filtered_records",
"field_scope": "selected_fields",
"search_scope": "fld_title(Title)",
},
},
},
}
reg.Register(searchStub)
if err := runShortcut(
t,
BaseRecordSearch,
[]string{
"+record-search",
"--base-token", "app_x",
"--table-id", "tbl_x",
"--json", `{"view_id":"vew_x","keyword":"Created","search_fields":["Title","fld_owner"],"select_fields":["Title","fld_owner"],"offset":0,"limit":2}`,
},
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, `"query_context"`) {
t.Fatalf("stdout=%s", got)
}
body := string(searchStub.CapturedBody)
if !strings.Contains(body, `"view_id":"vew_x"`) ||
!strings.Contains(body, `"keyword":"Created"`) ||
!strings.Contains(body, `"search_fields":["Title","fld_owner"]`) ||
!strings.Contains(body, `"select_fields":["Title","fld_owner"]`) ||
!strings.Contains(body, `"offset":0`) ||
!strings.Contains(body, `"limit":2`) {
t.Fatalf("captured body=%s", body)
}
})

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)
Expand Down
13 changes: 6 additions & 7 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-get", "+record-upsert", "+record-upload-attachment", "+record-delete",
"+record-list", "+record-search", "+record-get", "+record-upsert", "+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 @@ -252,18 +252,17 @@ 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-id")
}
if BaseRecordSearch.Validate != nil {
t.Fatalf("record search validate should be nil for API passthrough")
}
if BaseRecordGet.Validate != nil {
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)
}
if err := BaseRecordUpsert.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "table-id": "tbl_1", "json": "{"}, nil, nil)); err != nil {
t.Fatalf("invalid record json should bypass CLI validate, err=%v", err)
if BaseRecordUpsert.Validate != nil {
t.Fatalf("record upsert validate should be nil for API passthrough")
}
}

Expand Down
25 changes: 24 additions & 1 deletion shortcuts/base/record_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ func dryRunRecordGet(_ context.Context, runtime *common.RuntimeContext) *common.
Set("record_id", runtime.Str("record-id"))
}

func dryRunRecordSearch(_ 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/search").
Body(body).
Set("base_token", runtime.Str("base-token")).
Set("table_id", baseTableID(runtime))
}

func dryRunRecordUpsert(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
pc := newParseCtx(runtime)
body, _ := parseJSONObject(pc, runtime.Str("json"), "json")
Expand Down Expand Up @@ -89,7 +99,6 @@ 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 @@ -121,6 +130,20 @@ func executeRecordGet(runtime *common.RuntimeContext) error {
return nil
}

func executeRecordSearch(runtime *common.RuntimeContext) error {
pc := newParseCtx(runtime)
body, err := parseJSONObject(pc, runtime.Str("json"), "json")
if err != nil {
return err
}
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "records", "search"), nil, body)
if err != nil {
return err
}
runtime.Out(data, nil)
return nil
}

func executeRecordUpsert(runtime *common.RuntimeContext) error {
pc := newParseCtx(runtime)
body, err := parseJSONObject(pc, runtime.Str("json"), "json")
Expand Down
32 changes: 32 additions & 0 deletions shortcuts/base/record_search.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 BaseRecordSearch = common.Shortcut{
Service: "base",
Command: "+record-search",
Description: "Search records in a table",
Risk: "read",
Scopes: []string{"base:record:read"},
AuthTypes: authTypes(),
Flags: []common.Flag{
baseTokenFlag(true),
tableRefFlag(true),
{Name: "json", Desc: "record search JSON object", Required: true},
},
Tips: []string{
`Example: --json '{"keyword":"Alice","search_fields":["Name"]}'`,
"Agent hint: use the lark-base skill's record-search guide for usage and limits.",
},
DryRun: dryRunRecordSearch,
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeRecordSearch(runtime)
},
}
3 changes: 0 additions & 3 deletions shortcuts/base/record_upsert.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ var BaseRecordUpsert = common.Shortcut{
`Example: --json '{"Name":"Alice"}'`,
"Agent hint: use the lark-base skill's record-upsert guide for usage and limits.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
return validateRecordJSON(runtime)
},
DryRun: dryRunRecordUpsert,
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeRecordUpsert(runtime)
Expand Down
1 change: 1 addition & 0 deletions shortcuts/base/shortcuts.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func Shortcuts() []common.Shortcut {
BaseViewSetCard,
BaseViewRename,
BaseRecordList,
BaseRecordSearch,
BaseRecordGet,
BaseRecordUpsert,
BaseRecordUploadAttachment,
Expand Down
22 changes: 13 additions & 9 deletions skills/lark-base/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ metadata:
- 临时统计 / 聚合分析 → `+data-query`
- 要把结果长期显示在表里 → formula 字段
- 用户明确要 lookup,或确实更适合 `from/select/where/aggregate` → lookup 字段
- 明细读取 / 导出 → `+record-list / +record-get`
- 明细读取 / 关键词检索 / 导出 → `+record-search / +record-list / +record-get`
2. **先拿结构,再写命令**
- 至少先拿当前表结构:`+field-list` 或 `+table-get`
- 跨表场景必须再查**目标表**的结构
Expand All @@ -35,7 +35,7 @@ metadata:

## Agent 禁止行为

- 不要把 `+record-list` 当聚合分析引擎
- 不要把 `+record-list / +record-search` 当聚合分析引擎
- 不要没读 guide 就直接创建 formula / lookup 字段
- 不要凭自然语言猜表名、字段名、公式表达式里的字段引用
- 不要把系统字段、formula 字段、lookup 字段当成 `+record-upsert` 的写入目标
Expand All @@ -62,8 +62,8 @@ metadata:
- 特征:要把结果长期显示在 Base 里,跟随记录自动更新。
3. **显式要求 Lookup,或确实要按 source/select/where/aggregate 建模** → 用 lookup 字段
- 默认仍优先考虑 formula。lookup 只在用户明确要求、或更符合固定查找配置时使用。
4. **原始记录读取 / 明细导出** → `+record-list / +record-get`
- 不要把 `+record-list` 当分析引擎;它负责取明细,不负责聚合计算。
4. **原始记录读取 / 关键词检索 / 明细导出** → `+record-search / +record-list / +record-get`
- 不要把 `+record-list / +record-search` 当分析引擎;它们负责取明细,不负责聚合计算。

## 公式 / Lookup 专项规则

Expand Down Expand Up @@ -112,9 +112,9 @@ metadata:
1. **只使用原子命令** — 使用 `+table-list / +table-get / +field-create / +record-upsert / +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` 组合完成筛选读取
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`
5. **对记录进行分析(涉及"最高/最低/总计/平均/排名/比较/数量"等分析意图)** — 先读 [lark-base-data-query.md](references/lark-base-data-query.md),通过 `+data-query` 进行数据筛选聚合的服务端计算
6. **聚合分析与取数互斥** — 需要分组统计 / SUM / MAX / AVG / COUNT 时,必须使用 `+data-query`(服务端计算),禁止用 `+record-list` 拉全量记录再手动计算;反之,`+data-query` 不返回原始记录,取数场景仍走 `+record-list / +record-get`
6. **聚合分析与取数互斥** — 需要分组统计 / SUM / MAX / AVG / COUNT 时,必须使用 `+data-query`(服务端计算),禁止用 `+record-list / +record-search` 拉全量记录再手动计算;反之,`+data-query` 不返回原始记录,取数场景走 `+record-search / +record-list / +record-get`
7. **所有 `+xxx-list` 禁止并发调用** — `+table-list / +field-list / +record-list / +view-list / +record-history-list / +role-list` 只能串行执行
8. **批量上限 500 条/次** — 同一表建议串行写入,并在批次间延迟 0.5–1 秒
9. **统一参数名** — 一律使用 `--base-token`,不使用旧 `--app-token`
Expand All @@ -140,9 +140,10 @@ metadata:
| 创建 / 更新字段 | `lark-cli base +field-create` / `+field-update` | 使用 `--json` |
| 创建 / 更新公式字段 | `lark-cli base +field-create` / `+field-update` | `type=formula`;先读 formula guide,再创建 / 更新 |
| 创建 / 更新 lookup 字段 | `lark-cli base +field-create` / `+field-update` | `type=lookup`;先读 lookup guide,再创建 / 更新,默认先判断 formula 是否更合适 |
| 关键词搜索记录 | `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 +data-query` | 不要用 `+record-list` 拉全量数据再手动计算,需使用 `+data-query` 走服务端计算 |
| 聚合分析 / 比较排序 / 求最值 / 筛选统计 | `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` | 按表和记录查询变更历史 |
| 按视图筛选查询 | `lark-cli base +view-set-filter` + `lark-cli base +record-list` | 组合调用 |
Expand All @@ -165,7 +166,9 @@ 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` 支持重复传参 --field-id 做字段筛选
- **记录读取字段筛选**:`+record-list` 支持重复传参 `--field-id` 做字段筛选
- **`+record-list / +record-search` 选择规则**:优先使用 `+record-list`;仅当用户给出明确搜索关键词时,才使用 `+record-search`
- **`+record-search` 使用规则**:仅通过 `--json` 传搜索请求体;`keyword/search_fields/offset/limit` 等字段合法性由 API 侧按 schema 校验
- **字段可写性先判断**:存储字段才可写;公式 / lookup / 系统字段默认只读,写记录时应跳过
- **公式能力要主动想到**:用户说“算一下”“生成标签”“判断是否异常”“跨表汇总”“按日期差预警”时,要先判断是否应该建公式字段,而不是只返回手工分析方案
- **lookup 不是默认首选**:lookup 只在用户明确要求或确实更适合固定查找模型时使用;常规计算、跨表聚合和条件判断优先 formula
Expand Down Expand Up @@ -281,6 +284,7 @@ https://{domain}/base/{base-token}?table={table-id}&view={view-id}
- [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) — 视图筛选配置
- [lark-base-record-list.md](references/lark-base-record-list.md) — 记录列表读取与分页
- [lark-base-record-search.md](references/lark-base-record-search.md) — 关键词搜索记录
- [lark-base-advperm-enable.md](references/lark-base-advperm-enable.md) — `+advperm-enable` 启用高级权限
- [lark-base-advperm-disable.md](references/lark-base-advperm-disable.md) — `+advperm-disable` 停用高级权限
- [lark-base-role-list.md](references/lark-base-role-list.md) — `+role-list` 列出角色
Expand All @@ -303,7 +307,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-search / +record-list / +record-get / +record-upsert / +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
14 changes: 8 additions & 6 deletions skills/lark-base/references/lark-base-record-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

分页列出一张表里的记录;可按视图过滤,也可按字段裁剪返回列。

> 默认优先使用 `+record-list`;仅当用户提供明确搜索关键词时,才使用 [lark-base-record-search.md](lark-base-record-search.md)。

## 返回关键字段

| 字段 | 类型 | 说明 |
Expand All @@ -30,16 +32,16 @@

```bash
lark-cli base +record-list \
--base-token app_xxx \
--table-id tbl_xxx \
--base-token XXXXXX \
--table-id tblXXX \
--offset 0 \
--limit 100

lark-cli base +record-list \
--base-token app_xxx \
--table-id tbl_xxx \
--view-id viw_xxx \
--field-id fld_status \
--base-token XXXXXX \
--table-id tblXXX \
--view-id vewXXX \
--field-id fldStatus \
--field-id 项目名称 \
--offset 0 \
--limit 50
Expand Down
Loading
Loading