diff --git a/shortcuts/base/base_execute_test.go b/shortcuts/base/base_execute_test.go index 5faa3739ef..a9a18f583c 100644 --- a/shortcuts/base/base_execute_test.go +++ b/shortcuts/base/base_execute_test.go @@ -1383,6 +1383,125 @@ func TestBaseFieldExecuteCRUD(t *testing.T) { } }) + t.Run("create array reports progress when a later field fails", func(t *testing.T) { + oldDelay := fieldCreateBatchDelay + fieldCreateBatchDelay = 0 + t.Cleanup(func() { fieldCreateBatchDelay = oldDelay }) + + runPartial := func(input, createdType, failedName string, failedResponse map[string]interface{}) map[string]interface{} { + t.Helper() + factory, stdout, reg := newExecuteFactory(t) + register := func(name string, response map[string]interface{}) { + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/fields", + BodyFilter: func(body []byte) bool { return strings.Contains(string(body), `"name":"`+name+`"`) }, + Body: response, + }) + } + register("A", map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{"id": "fld_a", "name": "A", "type": createdType}, + }) + register(failedName, failedResponse) + err := runShortcut(t, BaseFieldCreate, []string{ + "+field-create", "--base-token", "app_x", "--table-id", "tbl_x", "--json", input, + }, factory, stdout) + var partialErr *output.PartialFailureError + if !errors.As(err, &partialErr) { + t.Fatalf("expected partial failure error, got %T: %v", err, err) + } + var envelope struct { + OK bool `json:"ok"` + Data map[string]interface{} `json:"data"` + } + if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil { + t.Fatalf("decode partial failure output: %v\nstdout=%s", err, stdout.String()) + } + if envelope.OK || envelope.Data == nil { + t.Fatalf("unexpected partial failure envelope: %#v", envelope) + } + return envelope.Data + } + + conflictResponse := map[string]interface{}{ + "code": 1254090, + "msg": "field already exists", + "error": map[string]interface{}{ + "log_id": "202607300001", + "troubleshooter": "https://open.feishu.cn/document/troubleshoot/field-exists", + "details": []interface{}{map[string]interface{}{"value": "choose a different field name"}}, + }, + } + data := runPartial(`[{"name":"A","type":"auto_number"},{"name":"B","type":"text"},{"name":"C","type":"text"}]`, "auto_number", "B", conflictResponse) + summary, _ := data["summary"].(map[string]interface{}) + if summary["requested"] != float64(3) || summary["attempted"] != float64(2) || + summary["created"] != float64(1) || summary["failed"] != float64(1) || summary["not_attempted"] != float64(1) { + t.Fatalf("unexpected summary: %#v", summary) + } + + items, _ := data["items"].([]interface{}) + if len(items) != 3 { + t.Fatalf("items=%#v, want three outcomes", items) + } + created, _ := items[0].(map[string]interface{}) + createdField, _ := created["field"].(map[string]interface{}) + failed, _ := items[1].(map[string]interface{}) + failedField, _ := failed["field"].(map[string]interface{}) + notAttempted, _ := items[2].(map[string]interface{}) + notAttemptedField, _ := notAttempted["field"].(map[string]interface{}) + if created["status"] != "created" || createdField["id"] != "fld_a" || + failed["status"] != "failed" || failed["index"] != float64(1) || failedField["name"] != "B" || + notAttempted["status"] != "not_attempted" || notAttemptedField["name"] != "C" { + t.Fatalf("unexpected item outcomes: %#v", items) + } + if !strings.Contains(common.GetString(failed, "error"), "field already exists") { + t.Fatalf("failed item must include the API error: %#v", failed) + } + for key, want := range map[string]interface{}{ + "type": "api", + "subtype": "unknown", + "code": float64(1254090), + "hint": "choose a different field name", + "retryable": false, + "log_id": "202607300001", + "troubleshooter": "https://open.feishu.cn/document/troubleshoot/field-exists", + } { + if failed[key] != want { + t.Fatalf("failed[%q]=%#v, want %#v; failed=%#v", key, failed[key], want, failed) + } + } + if _, ok := failed["error_type"]; ok { + t.Fatalf("failed item must use canonical type/subtype fields: %#v", failed) + } + if !strings.Contains(data["hint"].(string), "Do not retry failed items unless retryable is true") { + t.Fatalf("hint=%#v", data["hint"]) + } + if data["field_get_recommended"] != true || data["next_step"] != "inspect_items" || data["verification_hint"] == nil { + t.Fatalf("partial success with auto_number must recommend readback: %#v", data) + } + + simpleData := runPartial(`[{"name":"A","type":"text"},{"name":"B","type":"text"},{"name":"C","type":"text"}]`, "text", "B", conflictResponse) + if simpleData["field_get_recommended"] != false || simpleData["next_step"] != "inspect_items" { + t.Fatalf("simple-field partial failure must inspect items, not report done: %#v", simpleData) + } + + permissionData := runPartial(`[{"name":"A","type":"text"},{"name":"P","type":"text"}]`, "text", "P", map[string]interface{}{ + "code": 99991672, + "msg": "app scope not applied", + "error": map[string]interface{}{ + "permission_violations": []interface{}{map[string]interface{}{"subject": "base:field:create"}}, + }, + }) + items, _ = permissionData["items"].([]interface{}) + permissionFailure, _ := items[1].(map[string]interface{}) + missingScopes, _ := permissionFailure["missing_scopes"].([]interface{}) + if len(missingScopes) != 1 || missingScopes[0] != "base:field:create" || + permissionFailure["identity"] != "bot" || common.GetString(permissionFailure, "console_url") == "" { + t.Fatalf("permission failure must retain typed extensions: %#v", permissionFailure) + } + }) + t.Run("create array with generated field recommends readback", func(t *testing.T) { oldDelay := fieldCreateBatchDelay fieldCreateBatchDelay = 0 diff --git a/shortcuts/base/base_shortcuts_test.go b/shortcuts/base/base_shortcuts_test.go index bc3e9441b1..de1ed1acca 100644 --- a/shortcuts/base/base_shortcuts_test.go +++ b/shortcuts/base/base_shortcuts_test.go @@ -246,10 +246,50 @@ func TestBaseHighRiskShortcutsTipsGuideAgents(t *testing.T) { } } -func TestBaseFieldCreateHelpHidesReadGuideFlag(t *testing.T) { +func TestBaseFieldCreateTipsGuideTypeSelectionByStoredValue(t *testing.T) { + tips := strings.Join(BaseFieldCreate.Tips, "\n") + for _, want := range []string{ + "+field-create defines storage schema only", + "a documented field type", + "value being stored", + "never from the field name or business purpose", + "use style only to format that type", + "Do not probe source code, web search, or raw OpenAPI", + "formula, lookup, link, workflow, or automation", + "only when the user explicitly requests", + "automatically populated, synchronized, or backfilled values", + } { + if !strings.Contains(tips, want) { + t.Fatalf("field-create tips should contain %q, got:\n%s", want, tips) + } + } + lowerTips := strings.ToLower(tips) + for _, caseArtifact := range []string{ + "base_table_", + "larkoffice.com/base/", + "grading_pass_rate", + "benchmark", + } { + if strings.Contains(lowerTips, caseArtifact) { + t.Fatalf("field-create tips should remain generic, found %q:\n%s", caseArtifact, tips) + } + } +} + +func TestBaseFieldCreateHelpDocumentsBatchAndHidesReadGuideFlag(t *testing.T) { parent := &cobra.Command{Use: "base"} BaseFieldCreate.Mount(parent, &cmdutil.Factory{}) cmd := parent.Commands()[0] + if !strings.Contains(cmd.Short, "one or more fields") { + t.Fatalf("help should describe creating one or more fields, got %q", cmd.Short) + } + jsonFlag := cmd.Flags().Lookup("json") + if jsonFlag == nil { + t.Fatal("flag json must exist") + } + if !strings.Contains(jsonFlag.Usage, "JSON object or non-empty array") { + t.Fatalf("json flag help should document object and array input, got %q", jsonFlag.Usage) + } if cmd.Flags().Lookup("i-have-read-guide") == nil { t.Fatalf("flag i-have-read-guide must exist for runtime validation") } diff --git a/shortcuts/base/base_skill_contract_test.go b/shortcuts/base/base_skill_contract_test.go new file mode 100644 index 0000000000..f049411621 --- /dev/null +++ b/shortcuts/base/base_skill_contract_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "strings" + "testing" + + "github.com/larksuite/cli/internal/vfs" +) + +func TestBaseSkillKeepsCreateSemanticsLiteralAndGeneric(t *testing.T) { + const skillPath = "../../skills/lark-base/SKILL.md" + content, err := vfs.ReadFile(skillPath) + if err != nil { + t.Fatalf("read lark-base skill: %v", err) + } + + skill := string(content) + normalizedSkill := strings.Join(strings.Fields(skill), " ") + for _, want := range []string{ + "用户要求“新增/创建”时", + "本轮 create 返回的对象、ID 或数量", + "同名目标已存在时报告冲突", + "不能把已有资源算作本轮新增", + "不能静默复用或更新", + "对每类资源只做一次必要盘点", + "只有命令明确返回逐项结果时才优先使用批量创建", + "继续配置本轮返回的 ID", + "明确要求确保存在、复用或更新", + } { + if !strings.Contains(normalizedSkill, want) { + t.Fatalf("lark-base skill missing %q", want) + } + } + + for _, forbidden := range []string{ + "base_table_", + "larkoffice.com/base/", + "grading_pass_rate", + } { + if strings.Contains(skill, forbidden) { + t.Fatalf("lark-base skill must remain generic, found %q", forbidden) + } + } +} + +func TestFieldCreateBatchContractStaysConsistentAcrossSkillAndReferences(t *testing.T) { + readNormalized := func(path string) string { + t.Helper() + content, err := vfs.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + return strings.Join(strings.Fields(string(content)), " ") + } + + skill := readNormalized("../../skills/lark-base/SKILL.md") + fieldJSON := readNormalized("../../skills/lark-base/references/lark-base-field-json.md") + fieldCreate := readNormalized("../../skills/lark-base/references/lark-base-field-create.md") + + for _, want := range []string{ + "同一表创建多个字段时,一次向 `+field-create --json` 传字段对象数组", + "只有命令明确返回逐项结果时才优先使用批量创建", + } { + if !strings.Contains(skill, want) { + t.Fatalf("lark-base skill missing %q", want) + } + } + for _, want := range []string{ + "单个字段定义始终是 JSON 对象", + "`+field-create --json` 接受一个字段对象或非空字段对象数组", + "`+field-update --json` 只接受一个字段对象", + } { + if !strings.Contains(fieldJSON, want) { + t.Fatalf("field JSON SSOT missing %q", want) + } + } + if strings.Contains(fieldJSON, "`--json` 必须是 JSON 对象") { + t.Fatal("field JSON SSOT must not apply the update-only top-level object rule to field-create") + } + for _, want := range []string{ + "遇到首个失败即停止且不自动回滚已创建字段", + "部分失败返回 `ok:false`", + "`summary`", + "`items`", + "`next_step:\"inspect_items\"`", + "`missing_scopes`、`identity`、`console_url`", + "只有 `retryable:true` 的 `failed` 项可重试", + "`not_attempted` 项应单独继续", + } { + if !strings.Contains(fieldCreate, want) { + t.Fatalf("field-create reference missing %q", want) + } + } +} diff --git a/shortcuts/base/data_query_guide_contract_test.go b/shortcuts/base/data_query_guide_contract_test.go new file mode 100644 index 0000000000..61c5600e81 --- /dev/null +++ b/shortcuts/base/data_query_guide_contract_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package base + +import ( + "strings" + "testing" + + "github.com/larksuite/cli/internal/vfs" +) + +func TestDataQueryQuickGuideCoversConditionValueShapesWithoutScenarioTemplate(t *testing.T) { + const guidePath = "../../skills/lark-base/references/lark-base-data-query-guide.md" + content, err := vfs.ReadFile(guidePath) + if err != nil { + t.Fatalf("read data-query quick guide: %v", err) + } + guide := string(content) + normalizedGuide := strings.Join(strings.Fields(guide), " ") + + for _, want := range []string{ + "Common `Condition.value` shapes", + "`is` / `isNot`", + "exactly one option name", + "`isGreater`", + "`isLess`", + "`isEmpty`", + "`isNotEmpty`", + "uses `[]`", + `["Today"]`, + `["ExactDate",""]`, + "Use relative date keywords only for relative requests", + "[lark-base-data-query.md](lark-base-data-query.md)", + } { + if !strings.Contains(normalizedGuide, want) { + t.Fatalf("quick guide missing %q", want) + } + } + + if len(content) > 6*1024 { + t.Fatalf("quick guide grew to %d bytes; keep full DSL details in lark-base-data-query.md", len(content)) + } + + for _, forbidden := range []string{ + "base_table_", + "bytedance.larkoffice.com/base/", + "Combine a date boundary and a status exclusion", + `""`, + `""`, + `""`, + } { + if strings.Contains(normalizedGuide, forbidden) { + t.Fatalf("quick guide must remain generic, found %q", forbidden) + } + } +} diff --git a/shortcuts/base/field_create.go b/shortcuts/base/field_create.go index 121117627e..f8b2d4cd46 100644 --- a/shortcuts/base/field_create.go +++ b/shortcuts/base/field_create.go @@ -12,19 +12,21 @@ import ( var BaseFieldCreate = common.Shortcut{ Service: "base", Command: "+field-create", - Description: "Create a field", + Description: "Create one or more fields", Risk: "write", Scopes: []string{"base:field:create"}, AuthTypes: authTypes(), Flags: []common.Flag{ baseTokenFlag(true), tableRefFlag(true), - {Name: "json", Desc: "field property JSON object", Required: true}, + {Name: "json", Desc: "field property JSON object or non-empty array of field objects", Required: true}, {Name: "i-have-read-guide", Type: "bool", Desc: "set only after you have read the formula/lookup guide for those field types", Hidden: true}, }, Tips: []string{ `Example text: lark-cli base +field-create --base-token --table-id --json '{"name":"Status","type":"text"}'`, `Example select: lark-cli base +field-create --base-token --table-id --json '{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo"},{"name":"Done"}]}'`, + `+field-create defines storage schema only: choose a documented field type from the value being stored, never from the field name or business purpose, and use style only to format that type.`, + `Do not probe source code, web search, or raw OpenAPI for a purpose-named field type. Explore formula, lookup, link, workflow, or automation only when the user explicitly requests derived, related, automatically populated, synchronized, or backfilled values.`, "Agent hint: use the lark-base skill's field-create guide for usage and limits.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { diff --git a/shortcuts/base/field_ops.go b/shortcuts/base/field_ops.go index 8285c542ed..af95142917 100644 --- a/shortcuts/base/field_ops.go +++ b/shortcuts/base/field_ops.go @@ -5,10 +5,12 @@ package base import ( "context" + "errors" "fmt" "strings" "time" + "github.com/larksuite/cli/errs" "github.com/larksuite/cli/shortcuts/common" ) @@ -168,6 +170,9 @@ func executeFieldCreate(runtime *common.RuntimeContext) error { } data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "tables", baseTableID(runtime), "fields"), nil, body) if err != nil { + if len(fields) > 0 { + return fieldCreatePartialFailure(runtime, bodies, fields, idx, err) + } return err } fields = append(fields, data) @@ -180,6 +185,83 @@ func executeFieldCreate(runtime *common.RuntimeContext) error { return nil } +func fieldCreatePartialFailure(runtime *common.RuntimeContext, bodies []map[string]interface{}, createdFields []interface{}, failedIndex int, err error) error { + items := make([]map[string]interface{}, 0, len(bodies)) + for idx, field := range createdFields { + items = append(items, map[string]interface{}{ + "index": idx, + "status": "created", + "field": field, + }) + } + + failed := map[string]interface{}{ + "index": failedIndex, + "status": "failed", + "field": fieldCreateInputIdentity(bodies[failedIndex]), + "error": err.Error(), + } + if problem, ok := errs.ProblemOf(err); ok { + failed["type"] = string(problem.Category) + failed["subtype"] = string(problem.Subtype) + failed["retryable"] = problem.Retryable + if problem.Code != 0 { + failed["code"] = problem.Code + } + if problem.Hint != "" { + failed["hint"] = problem.Hint + } + if problem.LogID != "" { + failed["log_id"] = problem.LogID + } + if problem.Troubleshooter != "" { + failed["troubleshooter"] = problem.Troubleshooter + } + } + var permissionError *errs.PermissionError + if errors.As(err, &permissionError) { + if len(permissionError.MissingScopes) > 0 { + failed["missing_scopes"] = permissionError.MissingScopes + } + if permissionError.Identity != "" { + failed["identity"] = permissionError.Identity + } + if permissionError.ConsoleURL != "" { + failed["console_url"] = permissionError.ConsoleURL + } + } + items = append(items, failed) + + for idx := failedIndex + 1; idx < len(bodies); idx++ { + items = append(items, map[string]interface{}{ + "index": idx, + "status": "not_attempted", + "field": fieldCreateInputIdentity(bodies[idx]), + }) + } + + result := fieldCreateBatchResult(map[string]interface{}{ + "summary": map[string]interface{}{ + "requested": len(bodies), + "attempted": failedIndex + 1, + "created": len(createdFields), + "failed": 1, + "not_attempted": len(bodies) - failedIndex - 1, + }, + "items": items, + "hint": "Some fields were already created and were not rolled back. Do not retry failed items unless retryable is true; submit not_attempted items separately.", + }, bodies[:len(createdFields)]) + result["next_step"] = "inspect_items" + return runtime.OutPartialFailure(result, nil) +} + +func fieldCreateInputIdentity(body map[string]interface{}) map[string]interface{} { + return map[string]interface{}{ + "name": body["name"], + "type": body["type"], + } +} + func parseFieldCreateBodies(pc *parseCtx, raw string) ([]map[string]interface{}, error) { bodies, err := parseObjectList(pc, raw, "json") if err != nil { diff --git a/skills/lark-base/SKILL.md b/skills/lark-base/SKILL.md index 19dcba9209..b8a4391d23 100644 --- a/skills/lark-base/SKILL.md +++ b/skills/lark-base/SKILL.md @@ -52,7 +52,7 @@ metadata: | 管理 Base 内资源目录 | `+base-block-create/move/rename/delete` | 创建或整理 Base 直接管理的 folder/table/docx/dashboard/workflow;资源内容继续用对应命令 | | 管理数据表 | `+table-list/get/create/update/delete` | 处理 table 的列出、详情、创建、重命名和删除 | | 列/查/删字段 | `+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) | +| 创建/更新字段 | `+field-create` / `+field-update` | 同一表创建多个字段时,一次向 `+field-create --json` 传字段对象数组,不要逐字段调用;必读 [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) | | 读记录明细 | `+record-get` / `+record-list` / `+record-search` | 涉及筛选、排序、Top/Bottom N、聚合、多表关联、全局结论时读 [lark-base-data-analysis-sop.md](references/lark-base-data-analysis-sop.md) | | 写记录 | `+record-upsert` / `+record-batch-create` / `+record-batch-update` | 必读 [lark-base-record-upsert.md](references/lark-base-record-upsert.md) / [lark-base-record-batch-create.md](references/lark-base-record-batch-create.md) / [lark-base-record-batch-update.md](references/lark-base-record-batch-update.md) 和 [lark-base-cell-value.md](references/lark-base-cell-value.md) | | 附件字段 | `+record-upload-attachment` / `+record-download-attachment` / `+record-remove-attachment` | 附件不要伪造成普通 CellValue;上传走本地文件,下载/删除按 file token 或字段定位 | @@ -105,6 +105,7 @@ metadata: ## 写入前置规则 - 优先用写入返回确认结果;返回信息不足或任务明确要求核验时,再读回。 +- 严格区分动作语义:用户要求“新增/创建”时,必须用本轮 create 返回的对象、ID 或数量确认完成;同名目标已存在时报告冲突,不能把已有资源算作本轮新增,也不能静默复用或更新。复合创建任务对每类资源只做一次必要盘点;只有命令明确返回逐项结果时才优先使用批量创建,并继续配置本轮返回的 ID;只有用户明确要求确保存在、复用或更新时,才操作同名已有资源。 - 写记录前先读字段结构;只写存储字段。系统字段、附件字段、`formula`、`lookup` 不作为普通记录写入目标。 - 附件上传、下载、删除走专用 `+record-*-attachment` 命令。 - 写字段前先读 [lark-base-field-json.md](references/lark-base-field-json.md);涉及 `formula` / `lookup` 时必须读 [formula-field-guide.md](references/formula-field-guide.md) / [lookup-field-guide.md](references/lookup-field-guide.md)。 diff --git a/skills/lark-base/references/lark-base-data-query-guide.md b/skills/lark-base/references/lark-base-data-query-guide.md index d20bf4dbcd..0ea15ff6fc 100644 --- a/skills/lark-base/references/lark-base-data-query-guide.md +++ b/skills/lark-base/references/lark-base-data-query-guide.md @@ -42,6 +42,14 @@ lark-cli base +data-query \ --dsl '{"datasource":{"type":"table","table":{"tableId":""}},"dimensions":[{"field_name":"Owner","alias":"owner"}],"measures":[{"field_name":"Amount","aggregation":"sum","alias":"total_amount"}],"filters":{"type":1,"conjunction":"and","conditions":[{"field_name":"Status","operator":"is","value":["Done"]}]},"shaper":{"format":"flat"}}' ``` +## Common filter values + +Common `Condition.value` shapes: select `is` / `isNot` uses exactly one option +name; datetime `is` / `isGreater` / `isLess` uses `["Today"]` or +`["ExactDate",""]`; `isEmpty` / `isNotEmpty` uses `[]`. +Use relative date keywords only for relative requests; see +[lark-base-data-query.md](lark-base-data-query.md) for other field types and operators. + Use `tableName` when the table ID is unavailable but the table name is known: ```bash diff --git a/skills/lark-base/references/lark-base-field-create.md b/skills/lark-base/references/lark-base-field-create.md index 4771458984..6cd7693b37 100644 --- a/skills/lark-base/references/lark-base-field-create.md +++ b/skills/lark-base/references/lark-base-field-create.md @@ -2,7 +2,7 @@ > **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 -创建一个字段。 +创建一个或多个字段;同一表的多个字段用一次 JSON 数组输入,不要逐字段调用。 ## Agent 最小工作流 @@ -29,6 +29,12 @@ lark-cli base +field-create \ --base-token \ --table-id \ --json '{"name":"负责人","type":"user","multiple":false,"default_value":[{"$slot":"current_user"}],"description":"用于标记记录的直接负责人;协作约定可参考[团队字段约定](https://example.com/field-spec)"}' + +# 多个字段复用相同字段 JSON 形状,一次传非空数组 +lark-cli base +field-create \ + --base-token \ + --table-id \ + --json '[{"name":"备注","type":"text"},{"name":"优先级","type":"select","multiple":false,"options":[{"name":"高"},{"name":"低"}]}]' ``` ## 参数 @@ -37,7 +43,8 @@ lark-cli base +field-create \ |------|------|------| | `--base-token ` | 是 | Base Token | | `--table-id ` | 是 | 表 ID 或表名 | -| `--json ` | 是 | 字段属性 JSON 对象 | +| `--json ` | 是 | 单个字段 JSON 对象,或多个字段对象组成的非空数组 | + ## API 入参详情 **HTTP 方法和路径:** @@ -48,8 +55,9 @@ POST /open-apis/base/v3/bases/:base_token/tables/:table_id/fields ## JSON 值规范 -- `--json` 必须是 **JSON 对象**,顶层直接传字段定义,不要再套一层。 -- 顶层最少包含:`name`、`type`。 +- `--json` 接受单个字段 **JSON 对象**,也接受多个字段对象组成的非空数组;不要再套 `fields` 等外层对象。 +- 数组按顺序创建字段,遇到首个失败即停止且不自动回滚已创建字段;需要原子写入时不要假设数组具备事务语义。 +- 每个字段对象最少包含:`name`、`type`。 - 所有字段类型都支持可选 `description`;支持纯文本,也支持 Markdown 链接,如 `协作约定可参考[团队字段约定](https://example.com/field-spec)`。 - 需要字段默认值时传 `default_value`,直接使用字段对应 CellValue;`datetime` / `user` 的动态填充用 `$slot`。完整规则见 [lark-base-field-json.md](lark-base-field-json.md)。 - `type` 不同,必填子字段不同: @@ -86,13 +94,14 @@ POST /open-apis/base/v3/bases/:base_token/tables/:table_id/fields ## 返回重点 -- 返回 `field` 和 `created: true`。 -- 如果返回 `field_get_recommended:false` 且 `next_step:"done"`,表示本次是简单字段创建,通常不需要立刻执行 `+field-get`。 -- 如果返回 `field_get_recommended:true` 或 `next_step:"field_get"`,按 `verification_hint` 读回字段;`formula`、`lookup`、`link`、`auto_number` 等计算、关联或生成型字段更适合读回确认服务端最终结构。 +- 单字段返回 `field` 和 `created: true`;多字段返回 `fields`、`total` 和 `created: true`。 +- 数组部分失败返回 `ok:false`、`summary` 和有序 `items`,保留已创建字段及 ID、失败项和未执行项。`failed` 项保留 `type`、`subtype`、`code`、`hint`、`retryable`、`log_id`、`troubleshooter`;权限错误还保留原错误已有的 `missing_scopes`、`identity`、`console_url`。 +- 部分失败统一返回 `next_step:"inspect_items"`;`field_get_recommended` 仅表示已创建字段是否建议读回。只有 `retryable:true` 的 `failed` 项可重试,`not_attempted` 项应单独继续。 +- 完整成功且返回 `field_get_recommended:false`、`next_step:"done"` 时,本次是简单字段创建,通常不需要立刻执行 `+field-get`。 +- `field_get_recommended:true` 表示完成当前 `next_step` 后按 `verification_hint` 读回;完整成功时 `next_step:"field_get"` 表示可直接读回。`formula`、`lookup`、`link`、`auto_number` 等字段更适合读回确认服务端最终结构。 ## 工作流 - 1. formula / lookup 字段必须先阅读对应指南;没读之前不要直接创建。 2. 创建简单字段时,优先相信命令返回;只有用户要求精确核对额外属性,或返回建议读回时,才继续执行 `+field-get`。 diff --git a/skills/lark-base/references/lark-base-field-json.md b/skills/lark-base/references/lark-base-field-json.md index e7d3b6c7a8..17ba6c20a4 100644 --- a/skills/lark-base/references/lark-base-field-json.md +++ b/skills/lark-base/references/lark-base-field-json.md @@ -6,8 +6,9 @@ ## 1. 顶层规则(必须遵守) -- `--json` 必须是 JSON 对象。 -- 顶层统一使用:`type` + `name` + 类型特有字段。 +- 单个字段定义始终是 JSON 对象,每个字段对象统一使用:`type` + `name` + 类型特有字段。 +- `+field-create --json` 接受一个字段对象或非空字段对象数组。 +- `+field-update --json` 只接受一个字段对象。 - 所有字段类型都支持可选 `description`;支持纯文本,也支持 Markdown 链接。 - 字段默认值使用 `default_value`,直接传对应 CellValue;支持范围只有 `text`、`number`、静态 `select`、`datetime`、`user`。清空默认值传 `null`;省略表示创建时不设置、更新时不修改。 - 不要使用旧结构:`field_name`、`property`、`ui_type`、数字枚举 `type`。