From 294a855992708b03c30721f14ca856feefd99030 Mon Sep 17 00:00:00 2001 From: lijiehong <905965287@qq.com> Date: Thu, 6 Aug 2026 22:17:39 +0800 Subject: [PATCH 1/4] feat(base): require --fields on +table-create A table created without --fields gets the platform default schema. Those default fields then sit in the table alongside every field the caller adds afterwards, and no field command removes them all, so the only clean recovery is to drop the table and start over. Make --fields required so the schema is declared up front, the way +base-create already recommends via --table-name + --fields. - Mark --fields Required on +table-create, and reject blank / non-array / empty-array values in Validate: cobra's MarkFlagRequired only checks that the flag was set, so --fields "" and --fields "[]" would still reach the API with no fields body and fall back to the default schema. - Validate runs ahead of the dry-run branch, so --dry-run can no longer preview an invocation the real call would reject. - Update the lark-base skill, e2e coverage notes and the live e2e helper. BREAKING CHANGE: `lark-cli base +table-create --base-token --name ` without --fields now fails with a validation error instead of creating a default-schema table. Callers that relied on create-empty-then-add-fields must pass the schema to --fields. Co-Authored-By: Claude Opus 5 (1M context) --- shortcuts/base/base_dryrun_ops_test.go | 3 +- shortcuts/base/base_shortcuts_test.go | 10 +-- shortcuts/base/table_create.go | 13 +++- shortcuts/base/table_create_test.go | 89 ++++++++++++++++++++++++++ shortcuts/base/table_ops.go | 21 ++++++ skills/lark-base/SKILL.md | 3 +- tests/cli_e2e/base/coverage.md | 2 +- tests/cli_e2e/base/helpers_test.go | 9 +-- 8 files changed, 137 insertions(+), 13 deletions(-) create mode 100644 shortcuts/base/table_create_test.go diff --git a/shortcuts/base/base_dryrun_ops_test.go b/shortcuts/base/base_dryrun_ops_test.go index 234ac84f9b..4f46715657 100644 --- a/shortcuts/base/base_dryrun_ops_test.go +++ b/shortcuts/base/base_dryrun_ops_test.go @@ -27,8 +27,9 @@ func TestDryRunTableOps(t *testing.T) { rt := newBaseTestRuntime(map[string]string{"base-token": "app_x", "table-id": "tbl_1", "name": "Orders"}, nil, nil) assertDryRunContains(t, dryRunTableGet(ctx, rt), "GET /open-apis/base/v3/bases/app_x/tables/tbl_1") - assertDryRunContains(t, dryRunTableCreate(ctx, rt), "POST /open-apis/base/v3/bases/app_x/tables") + // +table-create requires --fields, so the fieldless shape is unreachable + // through the command surface; see table_create_test.go for that contract. tableCreateWithFieldsRT := newBaseTestRuntime( map[string]string{"base-token": "app_x", "name": "Orders", "fields": `[{"name":"Title","type":"text"}]`}, nil, diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index 51d38f89c6..b906212a93 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -746,7 +746,7 @@ func TestBaseJSONExamplesLiveInFlagDescriptions(t *testing.T) { name: "table create fields", shortcut: BaseTableCreate, wantHelp: []string{ - `field JSON array for create, e.g. [{"name":"Title","type":"text"}`, + `field JSON array defining the table schema; must hold at least one field, e.g. [{"name":"Title","type":"text"}`, }, }, { @@ -1219,10 +1219,12 @@ func TestBaseFieldValidate(t *testing.T) { func TestBaseTableValidate(t *testing.T) { ctx := context.Background() - if err := BaseTableCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "name": "Orders", "fields": "{"}, nil, nil)); err != nil { - t.Fatalf("invalid fields json should bypass CLI validate, err=%v", err) + // --fields carries the whole table schema, so it is parsed at validate time: + // an unusable schema must fail before the table exists, not after. + if err := BaseTableCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "name": "Orders", "fields": "{"}, nil, nil)); err == nil { + t.Fatal("invalid fields json should fail CLI validate") } - if err := BaseTableCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "name": "Orders", "view": `[1]`}, nil, nil)); err != nil { + if err := BaseTableCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "name": "Orders", "fields": `[{"name":"Name","type":"text"}]`, "view": `[1]`}, nil, nil)); err != nil { t.Fatalf("invalid view json should bypass CLI validate, err=%v", err) } if err := BaseTableCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "name": "Orders", "fields": `[{"name":"Name","type":"text"}]`, "view": `{"name":"Main"}`}, nil, nil)); err != nil { diff --git a/shortcuts/base/table_create.go b/shortcuts/base/table_create.go index 2175080e14..f30956be7b 100644 --- a/shortcuts/base/table_create.go +++ b/shortcuts/base/table_create.go @@ -9,10 +9,17 @@ import ( "github.com/larksuite/cli/shortcuts/common" ) +// BaseTableCreate creates a table with an explicit schema. --fields is required +// at the cli surface (cobra MarkFlagRequired); a missing flag fails before +// Validate runs with cobra's standard "required flag(s)" error (which the +// dispatcher classifies as a typed *errs.ValidationError). validateTableCreate +// still rejects blank, non-array and empty-array values, because cobra accepts +// --fields "" and --fields "[]" — both of which would reach the API without a +// fields body and get the platform default schema instead of the caller's. var BaseTableCreate = common.Shortcut{ Service: "base", Command: "+table-create", - Description: "Create a table and optional fields/views", + Description: "Create a table with an explicit field schema, plus optional views", Risk: "write", Scopes: []string{"base:table:create", "base:field:read", "base:field:create", "base:field:update", "base:view:write_only"}, AuthTypes: authTypes(), @@ -20,9 +27,11 @@ var BaseTableCreate = common.Shortcut{ baseTokenFlag(true), {Name: "name", Desc: "table name", Required: true}, {Name: "view", Desc: "view JSON object/array for create"}, - {Name: "fields", Desc: `field JSON array for create, e.g. [{"name":"Title","type":"text"},{"name":"Status","type":"select","options":[{"name":"Todo"},{"name":"Done"}]}]`}, + {Name: "fields", Required: true, Desc: `field JSON array defining the table schema; must hold at least one field, e.g. [{"name":"Title","type":"text"},{"name":"Status","type":"select","options":[{"name":"Todo"},{"name":"Done"}]}]`}, }, Tips: []string{ + `Example: lark-cli base +table-create --base-token --name "Tasks" --fields '[{"name":"Title","type":"text"},{"name":"Status","type":"select","options":[{"name":"Todo"},{"name":"Done"}]}]'`, + "--fields is required: a table created without it gets the platform default schema, and those default fields stay in the table alongside the ones you add.", "Before using --fields, read lark-base-field-json.md or rely on the same field JSON shape used by +field-create; do not invent field properties.", "The first --fields item becomes the primary field.", }, diff --git a/shortcuts/base/table_create_test.go b/shortcuts/base/table_create_test.go new file mode 100644 index 0000000000..807312096b --- /dev/null +++ b/shortcuts/base/table_create_test.go @@ -0,0 +1,89 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "strings" + "testing" + + "github.com/larksuite/cli/shortcuts/common" +) + +func tableCreateFlag(t *testing.T, name string) common.Flag { + t.Helper() + for _, flag := range BaseTableCreate.Flags { + if flag.Name == name { + return flag + } + } + t.Fatalf("+table-create has no --%s flag", name) + return common.Flag{} +} + +// The declaration itself is the contract: --fields must stay cobra-required so +// `--help` and the machine-readable schema both advertise it, not just Validate. +func TestBaseTableCreateDeclaresFieldsRequired(t *testing.T) { + if !tableCreateFlag(t, "fields").Required { + t.Fatal("--fields must be declared Required on +table-create") + } +} + +func TestBaseTableCreateRejectsMissingFields(t *testing.T) { + factory, stdout, _ := newExecuteFactory(t) + + err := runShortcut(t, BaseTableCreate, []string{"+table-create", "--base-token", "app_x", "--name", "Orders"}, factory, stdout) + if err == nil { + t.Fatal("expected +table-create without --fields to fail") + } + if !strings.Contains(err.Error(), `required flag(s) "fields" not set`) { + t.Fatalf("err=%v, want cobra required-flag error for fields", err) + } +} + +// cobra's MarkFlagRequired only checks that the flag was set, so blank and +// empty-array values still reach Validate. Both would otherwise create a table +// with the platform default schema instead of the caller's. +func TestBaseTableCreateRejectsBlankFields(t *testing.T) { + factory, stdout, _ := newExecuteFactory(t) + + err := runShortcut(t, BaseTableCreate, []string{"+table-create", "--base-token", "app_x", "--name", "Orders", "--fields", " "}, factory, stdout) + assertInvalidArgumentValidation(t, err, "--fields", nil, "cannot be blank") +} + +func TestBaseTableCreateRejectsEmptyFieldsArray(t *testing.T) { + factory, stdout, _ := newExecuteFactory(t) + + err := runShortcut(t, BaseTableCreate, []string{"+table-create", "--base-token", "app_x", "--name", "Orders", "--fields", "[]"}, factory, stdout) + assertInvalidArgumentValidation(t, err, "--fields", nil, "at least one field") +} + +func TestBaseTableCreateRejectsNonObjectFieldItem(t *testing.T) { + factory, stdout, _ := newExecuteFactory(t) + + err := runShortcut(t, BaseTableCreate, []string{"+table-create", "--base-token", "app_x", "--name", "Orders", "--fields", `["Title"]`}, factory, stdout) + assertInvalidArgumentValidation(t, err, "--fields", nil, "must be an object") +} + +// Validate runs ahead of the dry-run branch, so --dry-run cannot be used to +// preview an invocation the real call would reject. +func TestBaseTableCreateDryRunRejectsEmptyFieldsArray(t *testing.T) { + factory, stdout, _ := newExecuteFactory(t) + + err := runShortcut(t, BaseTableCreate, []string{"+table-create", "--base-token", "app_x", "--name", "Orders", "--fields", "[]", "--dry-run"}, factory, stdout) + assertInvalidArgumentValidation(t, err, "--fields", nil, "at least one field") + if stdout.Len() != 0 { + t.Fatalf("rejected dry-run must not print a request preview, stdout=%s", stdout.String()) + } +} + +func TestBaseTableCreateValidateAcceptsFieldSchema(t *testing.T) { + runtime := newBaseTestRuntime(map[string]string{ + "base-token": "app_x", + "name": "Orders", + "fields": `[{"name":"OrderNo","type":"text"}]`, + }, nil, nil) + if err := validateTableCreate(runtime); err != nil { + t.Fatalf("valid field schema rejected: %v", err) + } +} diff --git a/shortcuts/base/table_ops.go b/shortcuts/base/table_ops.go index 586bc57287..67a815e3ec 100644 --- a/shortcuts/base/table_ops.go +++ b/shortcuts/base/table_ops.go @@ -7,6 +7,7 @@ import ( "context" "strings" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/shortcuts/common" ) @@ -54,6 +55,26 @@ func dryRunTableDelete(_ context.Context, runtime *common.RuntimeContext) *commo } func validateTableCreate(runtime *common.RuntimeContext) error { + raw := strings.TrimSpace(runtime.Str("fields")) + if raw == "" { + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--fields is required and cannot be blank"). + WithParam("--fields"). + WithHint(`Pass the table schema as a JSON array, e.g. --fields '[{"name":"Title","type":"text"}]'. Read lark-base-field-json.md for the field JSON shape.`) + } + items, err := parseJSONArray(newParseCtx(runtime), raw, "fields") + if err != nil { + return err + } + if len(items) == 0 { + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--fields must define at least one field"). + WithParam("--fields"). + WithHint("An empty array is not a schema: the table would be created with the platform default schema instead of the one you asked for.") + } + for idx, item := range items { + if _, ok := item.(map[string]interface{}); !ok { + return baseValidationErrorf("--fields item %d must be an object", idx+1) + } + } return nil } diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index d51bf8cc85..ccdba69ca2 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -54,7 +54,7 @@ metadata: | Base 文件导入/导出 | 转 `lark-drive` | 文件格式、参数、路径限制和仅结构导出规则由 `lark-drive` 负责;在线复制走 `+base-copy` | | 查看 Base 内资源目录 | `+base-block-list` | 想先了解一个 Base 里有哪些 table/docx/dashboard/workflow/folder 时优先用它;返回 ID 关系和 fewshot 看 `--help` | | 管理 Base 内资源目录 | `+base-block-create/move/rename/delete` | 创建或整理 Base 直接管理的 folder/table/docx/dashboard/workflow;资源内容继续用对应命令 | -| 管理数据表 | `+table-list/get/create/update/delete` | 处理 table 的列出、详情、创建、重命名和删除 | +| 管理数据表 | `+table-list/get/create/update/delete` | 处理 table 的列出、详情、创建、重命名和删除;`+table-create` 必须传 `--fields` 一次性定义表结构,字段 JSON 读 [lark-base-field-json.md](references/lark-base-field-json.md) | | 复制 Base 内单张数据表 | `+table-copy` / `+table-copy-status` | 默认只复制结构;只有用户明确要求复制全表、数据、行或记录时才传 `--range all`;异步任务按返回的 `task_id` 查询或续等 | | 列/查/删字段 | `+field-list/get/delete/search-options` | 写入前用 list/get 确认字段类型、选项、ID;删除前确认目标字段 | | 创建/更新字段 | `+field-create` / `+field-update` | 必读 [lark-base-field-json.md](references/lark-base-field-json.md);公式读 [formula-field-guide.md](references/formula-field-guide.md);lookup 读 [lookup-field-guide.md](references/lookup-field-guide.md);命令细节读 [lark-base-field-create.md](references/lark-base-field-create.md) / [lark-base-field-update.md](references/lark-base-field-update.md) | @@ -81,6 +81,7 @@ metadata: - `base-block` 只负责资源目录管理,包括创建资源、移动到 folder、重命名和删除;具体资源内容仍走 table/dashboard/workflow 命令。 - 新建 Base 时,强烈推荐一次性执行 `lark-cli base +base-create --name "" --table-name "" --fields ''`,同时配置新 Base 里唯一一个初始数据表的 name 和 schema;使用 `--fields` 前先读 [lark-base-field-json.md](references/lark-base-field-json.md) 或复用 `+field-create` 的字段 JSON 形状,不要猜字段属性。 - `+base-create` 不传 `--table-name` 和 `--fields` 时,会创建一个默认 schema 的初始数据表。 +- 在已有 Base 中建表时,`+table-create` 的 `--fields` 是必填项:一次性传完整字段数组,不要先建空表再逐个补字段。省略 `--fields` 建出来的表带平台默认 schema,那些默认字段会和你后补的字段并存。 - `+table-copy` 的安全默认值是只复制表结构;用户没有明确要求记录时省略 `--range`,明确要求包含记录时才传 `--range all`。`--table-id` 可直接使用当前 Base 中的表 ID 或表名。 - 表、字段、视图、workflow、dashboard block 的名称和 ID 必须来自真实返回,不要凭用户口述猜。 - 存储字段可写;系统字段、`formula`、`lookup` 只读;附件字段走专用 attachment 命令。 diff --git a/tests/cli_e2e/base/coverage.md b/tests/cli_e2e/base/coverage.md index 578dee6012..880b05c163 100644 --- a/tests/cli_e2e/base/coverage.md +++ b/tests/cli_e2e/base/coverage.md @@ -83,7 +83,7 @@ | ✓ | base +role-get | shortcut | base_role_workflow_test.go::TestBase_RoleWorkflow/get as bot | `--base-token`; `--role-id` | | | ✓ | base +role-list | shortcut | base_role_workflow_test.go::TestBase_RoleWorkflow/list as bot | `--base-token` | | | ✓ | base +role-update | shortcut | base_role_workflow_test.go::TestBase_RoleWorkflow/update as bot | `--base-token`; `--role-id`; `--json` | | -| ✓ | base +table-create | shortcut | base/helpers_test.go::createTableWithRetry | `--base-token`; `--name`; optional `--fields`; optional `--view` | helper asserts table id | +| ✓ | base +table-create | shortcut | base/helpers_test.go::createTableWithRetry | `--base-token`; `--name`; `--fields` (required); optional `--view` | helper asserts table id | | ✓ | base +table-copy | shortcut | base_table_copy_dryrun_test.go::TestBaseTableCopyDryRun; base_table_copy_workflow_test.go::TestBaseTableCopyWorkflow | `--table-id` ID/name; default `--range schema`; explicit `--range all --wait --timeout` | dry-run covered; live workflow is deployment-gated and not yet verified | | ✓ | base +table-copy-status | shortcut | base_table_copy_dryrun_test.go::TestBaseTableCopyDryRun/status; base_table_copy_workflow_test.go::TestBaseTableCopyWorkflow/all no-wait and status | opaque `--task-id` | dry-run covered; live polling is deployment-gated and not yet verified | | ✕ | base +table-delete | shortcut | | none | cleanup only | diff --git a/tests/cli_e2e/base/helpers_test.go b/tests/cli_e2e/base/helpers_test.go index ddfca24cb5..4ee83bde1e 100644 --- a/tests/cli_e2e/base/helpers_test.go +++ b/tests/cli_e2e/base/helpers_test.go @@ -127,10 +127,11 @@ func createBaseWithRetry(t *testing.T, ctx context.Context, name string) string func createTableWithRetry(t *testing.T, parentT *testing.T, ctx context.Context, baseToken string, name string, fieldsJSON string, viewJSON string) (tableID string, primaryFieldID string, primaryViewID string) { t.Helper() - args := []string{"base", "+table-create", "--base-token", baseToken, "--name", name} - if fieldsJSON != "" { - args = append(args, "--fields", fieldsJSON) - } + // +table-create requires --fields; an empty schema here would fail at the + // CLI surface with a validation error rather than exercising the API. + require.NotEmpty(t, fieldsJSON, "createTableWithRetry requires a field schema") + + args := []string{"base", "+table-create", "--base-token", baseToken, "--name", name, "--fields", fieldsJSON} if viewJSON != "" { args = append(args, "--view", viewJSON) } From 6950fcae7bc11f4fa280369631aa721002db05d3 Mon Sep 17 00:00:00 2001 From: lijiehong <905965287@qq.com> Date: Fri, 7 Aug 2026 00:05:37 +0800 Subject: [PATCH 2/4] test(base): pin typed metadata on the invalid-schema rejection Address review feedback on the +table-create schema validation. - The invalid-fields-JSON rejection now asserts category, subtype and param through assertInvalidArgumentValidation, plus the preserved *json.SyntaxError cause, instead of only asserting that some error came back. - Document why the missing-flag test asserts cobra's text rather than errs metadata, and pin that layer boundary: cobra's ValidateRequiredFlags emits a plain error and the dispatcher types it later (cmd/root_test.go). The test now fails if that boundary moves, so the weaker assertion cannot silently outlive its reason. - Reword the --fields tip and the lark-base skill note: both described the fieldless path as if it were still reachable through +table-create. They now say the command rejects omitted / blank / empty schemas up front, while keeping why the schema must be declared here. Co-Authored-By: Claude Opus 5 (1M context) --- shortcuts/base/base_shortcuts_test.go | 11 ++++++++--- shortcuts/base/table_create.go | 2 +- shortcuts/base/table_create_test.go | 10 ++++++++++ skills/lark-base/SKILL.md | 2 +- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index b906212a93..6cbefb6926 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -1220,9 +1220,14 @@ func TestBaseFieldValidate(t *testing.T) { func TestBaseTableValidate(t *testing.T) { ctx := context.Background() // --fields carries the whole table schema, so it is parsed at validate time: - // an unusable schema must fail before the table exists, not after. - if err := BaseTableCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "name": "Orders", "fields": "{"}, nil, nil)); err == nil { - t.Fatal("invalid fields json should fail CLI validate") + // an unusable schema must fail before the table exists, not after. The + // rejection has to stay machine-readable, so assert the typed metadata and + // the preserved parse cause rather than the message alone. + err := BaseTableCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "name": "Orders", "fields": "{"}, nil, nil)) + assertInvalidArgumentValidation(t, err, "--fields", []string{"--fields"}, "invalid JSON array") + var syntaxErr *json.SyntaxError + if !errors.As(err, &syntaxErr) { + t.Fatalf("invalid fields json must preserve the json parse cause, err=%v", err) } if err := BaseTableCreate.Validate(ctx, newBaseTestRuntime(map[string]string{"base-token": "b", "name": "Orders", "fields": `[{"name":"Name","type":"text"}]`, "view": `[1]`}, nil, nil)); err != nil { t.Fatalf("invalid view json should bypass CLI validate, err=%v", err) diff --git a/shortcuts/base/table_create.go b/shortcuts/base/table_create.go index f30956be7b..0985a372db 100644 --- a/shortcuts/base/table_create.go +++ b/shortcuts/base/table_create.go @@ -31,7 +31,7 @@ var BaseTableCreate = common.Shortcut{ }, Tips: []string{ `Example: lark-cli base +table-create --base-token --name "Tasks" --fields '[{"name":"Title","type":"text"},{"name":"Status","type":"select","options":[{"name":"Todo"},{"name":"Done"}]}]'`, - "--fields is required: a table created without it gets the platform default schema, and those default fields stay in the table alongside the ones you add.", + "--fields is required and must hold at least one field; omitted, blank and empty-array schemas are rejected before any table is created. Declaring the schema here is the only way to avoid the platform default fields, which stay in the table alongside anything you add later.", "Before using --fields, read lark-base-field-json.md or rely on the same field JSON shape used by +field-create; do not invent field properties.", "The first --fields item becomes the primary field.", }, diff --git a/shortcuts/base/table_create_test.go b/shortcuts/base/table_create_test.go index 807312096b..baf5ec7b25 100644 --- a/shortcuts/base/table_create_test.go +++ b/shortcuts/base/table_create_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/shortcuts/common" ) @@ -29,6 +30,12 @@ func TestBaseTableCreateDeclaresFieldsRequired(t *testing.T) { } } +// A missing flag fails inside cobra's ValidateRequiredFlags, which emits a plain +// error — the typed envelope is applied later by the dispatcher, whose +// classification of "required flag(s)" is pinned in cmd/root_test.go. So this +// test asserts the text contract the dispatcher keys on, and pins the layer +// boundary itself; asserting errs metadata here would test a promise this layer +// does not make. func TestBaseTableCreateRejectsMissingFields(t *testing.T) { factory, stdout, _ := newExecuteFactory(t) @@ -39,6 +46,9 @@ func TestBaseTableCreateRejectsMissingFields(t *testing.T) { if !strings.Contains(err.Error(), `required flag(s) "fields" not set`) { t.Fatalf("err=%v, want cobra required-flag error for fields", err) } + if _, typed := errs.ProblemOf(err); typed { + t.Fatal("cobra required-flag errors reach the dispatcher untyped; if that changed, assert the typed metadata here instead") + } } // cobra's MarkFlagRequired only checks that the flag was set, so blank and diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index ccdba69ca2..1393575d82 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -81,7 +81,7 @@ metadata: - `base-block` 只负责资源目录管理,包括创建资源、移动到 folder、重命名和删除;具体资源内容仍走 table/dashboard/workflow 命令。 - 新建 Base 时,强烈推荐一次性执行 `lark-cli base +base-create --name "" --table-name "
" --fields ''`,同时配置新 Base 里唯一一个初始数据表的 name 和 schema;使用 `--fields` 前先读 [lark-base-field-json.md](references/lark-base-field-json.md) 或复用 `+field-create` 的字段 JSON 形状,不要猜字段属性。 - `+base-create` 不传 `--table-name` 和 `--fields` 时,会创建一个默认 schema 的初始数据表。 -- 在已有 Base 中建表时,`+table-create` 的 `--fields` 是必填项:一次性传完整字段数组,不要先建空表再逐个补字段。省略 `--fields` 建出来的表带平台默认 schema,那些默认字段会和你后补的字段并存。 +- 在已有 Base 中建表时,`+table-create` 的 `--fields` 是必填项:一次性传完整字段数组,不要先建空表再逐个补字段。省略、留空或传空数组都会在建表前被直接拒绝——因为只有在这里声明 schema 才能避开平台默认字段,那些默认字段一旦生成就会和你后补的字段并存。 - `+table-copy` 的安全默认值是只复制表结构;用户没有明确要求记录时省略 `--range`,明确要求包含记录时才传 `--range all`。`--table-id` 可直接使用当前 Base 中的表 ID 或表名。 - 表、字段、视图、workflow、dashboard block 的名称和 ID 必须来自真实返回,不要凭用户口述猜。 - 存储字段可写;系统字段、`formula`、`lookup` 只读;附件字段走专用 attachment 命令。 From 1d3bedcc34156584252ece3bec81d6aec90925e3 Mon Sep 17 00:00:00 2001 From: lijiehong <905965287@qq.com> Date: Fri, 7 Aug 2026 11:46:52 +0800 Subject: [PATCH 3/4] docs(base): enrich +table-create field example and drop redundant tips The cobra Required declaration and flag Desc already advertise the --fields requirement, so the tips paragraph restating it (and its SKILL.md / coverage.md echoes) is dropped. The select-field example now carries multiple/hue/lightness so agents copy a complete option shape. --- shortcuts/base/table_create.go | 5 ++--- skills/lark-base/SKILL.md | 1 - tests/cli_e2e/base/coverage.md | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/shortcuts/base/table_create.go b/shortcuts/base/table_create.go index 0985a372db..d24ad837b4 100644 --- a/shortcuts/base/table_create.go +++ b/shortcuts/base/table_create.go @@ -27,11 +27,10 @@ var BaseTableCreate = common.Shortcut{ baseTokenFlag(true), {Name: "name", Desc: "table name", Required: true}, {Name: "view", Desc: "view JSON object/array for create"}, - {Name: "fields", Required: true, Desc: `field JSON array defining the table schema; must hold at least one field, e.g. [{"name":"Title","type":"text"},{"name":"Status","type":"select","options":[{"name":"Todo"},{"name":"Done"}]}]`}, + {Name: "fields", Required: true, Desc: `field JSON array defining the table schema; must hold at least one field, e.g. [{"name":"Title","type":"text"},{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo","hue":"Blue","lightness":"Lighter"},{"name":"Done","hue":"Green","lightness":"Light"}]}]`}, }, Tips: []string{ - `Example: lark-cli base +table-create --base-token --name "Tasks" --fields '[{"name":"Title","type":"text"},{"name":"Status","type":"select","options":[{"name":"Todo"},{"name":"Done"}]}]'`, - "--fields is required and must hold at least one field; omitted, blank and empty-array schemas are rejected before any table is created. Declaring the schema here is the only way to avoid the platform default fields, which stay in the table alongside anything you add later.", + `Example: lark-cli base +table-create --base-token --name "Tasks" --fields '[{"name":"Title","type":"text"},{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo","hue":"Blue","lightness":"Lighter"},{"name":"Done","hue":"Green","lightness":"Light"}]}]'`, "Before using --fields, read lark-base-field-json.md or rely on the same field JSON shape used by +field-create; do not invent field properties.", "The first --fields item becomes the primary field.", }, diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index 1393575d82..dc0bd18c5b 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -81,7 +81,6 @@ metadata: - `base-block` 只负责资源目录管理,包括创建资源、移动到 folder、重命名和删除;具体资源内容仍走 table/dashboard/workflow 命令。 - 新建 Base 时,强烈推荐一次性执行 `lark-cli base +base-create --name "" --table-name "
" --fields ''`,同时配置新 Base 里唯一一个初始数据表的 name 和 schema;使用 `--fields` 前先读 [lark-base-field-json.md](references/lark-base-field-json.md) 或复用 `+field-create` 的字段 JSON 形状,不要猜字段属性。 - `+base-create` 不传 `--table-name` 和 `--fields` 时,会创建一个默认 schema 的初始数据表。 -- 在已有 Base 中建表时,`+table-create` 的 `--fields` 是必填项:一次性传完整字段数组,不要先建空表再逐个补字段。省略、留空或传空数组都会在建表前被直接拒绝——因为只有在这里声明 schema 才能避开平台默认字段,那些默认字段一旦生成就会和你后补的字段并存。 - `+table-copy` 的安全默认值是只复制表结构;用户没有明确要求记录时省略 `--range`,明确要求包含记录时才传 `--range all`。`--table-id` 可直接使用当前 Base 中的表 ID 或表名。 - 表、字段、视图、workflow、dashboard block 的名称和 ID 必须来自真实返回,不要凭用户口述猜。 - 存储字段可写;系统字段、`formula`、`lookup` 只读;附件字段走专用 attachment 命令。 diff --git a/tests/cli_e2e/base/coverage.md b/tests/cli_e2e/base/coverage.md index 880b05c163..87c7dd6f66 100644 --- a/tests/cli_e2e/base/coverage.md +++ b/tests/cli_e2e/base/coverage.md @@ -83,7 +83,7 @@ | ✓ | base +role-get | shortcut | base_role_workflow_test.go::TestBase_RoleWorkflow/get as bot | `--base-token`; `--role-id` | | | ✓ | base +role-list | shortcut | base_role_workflow_test.go::TestBase_RoleWorkflow/list as bot | `--base-token` | | | ✓ | base +role-update | shortcut | base_role_workflow_test.go::TestBase_RoleWorkflow/update as bot | `--base-token`; `--role-id`; `--json` | | -| ✓ | base +table-create | shortcut | base/helpers_test.go::createTableWithRetry | `--base-token`; `--name`; `--fields` (required); optional `--view` | helper asserts table id | +| ✓ | base +table-create | shortcut | base/helpers_test.go::createTableWithRetry | `--base-token`; `--name`; `--fields`; optional `--view` | helper asserts table id | | ✓ | base +table-copy | shortcut | base_table_copy_dryrun_test.go::TestBaseTableCopyDryRun; base_table_copy_workflow_test.go::TestBaseTableCopyWorkflow | `--table-id` ID/name; default `--range schema`; explicit `--range all --wait --timeout` | dry-run covered; live workflow is deployment-gated and not yet verified | | ✓ | base +table-copy-status | shortcut | base_table_copy_dryrun_test.go::TestBaseTableCopyDryRun/status; base_table_copy_workflow_test.go::TestBaseTableCopyWorkflow/all no-wait and status | opaque `--task-id` | dry-run covered; live polling is deployment-gated and not yet verified | | ✕ | base +table-delete | shortcut | | none | cleanup only | From c7b76a7a521efb4a883c7b1f3453810f50b5a5a2 Mon Sep 17 00:00:00 2001 From: lijiehong <905965287@qq.com> Date: Fri, 7 Aug 2026 12:01:36 +0800 Subject: [PATCH 4/4] chore: remove useless example --- shortcuts/base/table_create.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shortcuts/base/table_create.go b/shortcuts/base/table_create.go index d24ad837b4..c67943af69 100644 --- a/shortcuts/base/table_create.go +++ b/shortcuts/base/table_create.go @@ -27,10 +27,9 @@ var BaseTableCreate = common.Shortcut{ baseTokenFlag(true), {Name: "name", Desc: "table name", Required: true}, {Name: "view", Desc: "view JSON object/array for create"}, - {Name: "fields", Required: true, Desc: `field JSON array defining the table schema; must hold at least one field, e.g. [{"name":"Title","type":"text"},{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo","hue":"Blue","lightness":"Lighter"},{"name":"Done","hue":"Green","lightness":"Light"}]}]`}, + {Name: "fields", Required: true, Desc: `field JSON array defining the table schema; must hold at least one field, e.g. [{"name":"Title","type":"text"},{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo"},{"name":"Done"}]}]`}, }, Tips: []string{ - `Example: lark-cli base +table-create --base-token --name "Tasks" --fields '[{"name":"Title","type":"text"},{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo","hue":"Blue","lightness":"Lighter"},{"name":"Done","hue":"Green","lightness":"Light"}]}]'`, "Before using --fields, read lark-base-field-json.md or rely on the same field JSON shape used by +field-create; do not invent field properties.", "The first --fields item becomes the primary field.", },