From 3feb70b32ade6773b097b4286bf253c52ac13649 Mon Sep 17 00:00:00 2001 From: yballul-bytedance Date: Tue, 16 Jun 2026 16:36:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(drive):=20=E6=94=AF=E6=8C=81=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=20Base=20=E7=BB=93=E6=9E=84=E5=BF=AB=E7=85=A7=20(#148?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 为 drive +export 增加 --only-schema 参数,并透传 only_schema 到导出任务请求。 2. 限制该参数仅用于 bitable 导出 .base,并补充单测与 dry-run E2E 覆盖。 Change-Id: I736cebf5841cc1c6acaa8a3ab16be51ba4cb355d --- shortcuts/drive/drive_export.go | 7 ++++ shortcuts/drive/drive_export_common.go | 8 ++++ shortcuts/drive/drive_export_test.go | 13 ++++++ .../references/lark-drive-export.md | 10 +++++ tests/cli_e2e/drive/coverage.md | 4 +- .../cli_e2e/drive/drive_export_dryrun_test.go | 41 +++++++++++++++++++ 6 files changed, 81 insertions(+), 2 deletions(-) diff --git a/shortcuts/drive/drive_export.go b/shortcuts/drive/drive_export.go index 86e7c99a5..d01044595 100644 --- a/shortcuts/drive/drive_export.go +++ b/shortcuts/drive/drive_export.go @@ -34,6 +34,7 @@ var DriveExport = common.Shortcut{ {Name: "doc-type", Desc: "source document type: doc | docx | sheet | bitable | slides", Required: true, Enum: []string{"doc", "docx", "sheet", "bitable", "slides"}}, {Name: "file-extension", Desc: "export format: docx | pdf | xlsx | csv | markdown | base (bitable only) | pptx (slides only)", Required: true, Enum: []string{"docx", "pdf", "xlsx", "csv", "markdown", "base", "pptx"}}, {Name: "sub-id", Desc: "sub-table/sheet ID, required when exporting sheet/bitable as csv"}, + {Name: "only-schema", Type: "bool", Desc: "export only bitable schema when --doc-type bitable --file-extension base"}, {Name: "file-name", Desc: "preferred output filename (optional)"}, {Name: "output-dir", Default: ".", Desc: "local output directory (default: current directory)"}, {Name: "overwrite", Type: "bool", Desc: "overwrite existing output file"}, @@ -44,6 +45,7 @@ var DriveExport = common.Shortcut{ DocType: runtime.Str("doc-type"), FileExtension: runtime.Str("file-extension"), SubID: runtime.Str("sub-id"), + OnlySchema: runtime.Bool("only-schema"), }) }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { @@ -52,6 +54,7 @@ var DriveExport = common.Shortcut{ DocType: runtime.Str("doc-type"), FileExtension: runtime.Str("file-extension"), SubID: runtime.Str("sub-id"), + OnlySchema: runtime.Bool("only-schema"), } // Markdown export is a special case: docx markdown comes from the V2 // docs_ai fetch API directly instead of the Drive export task API. @@ -78,6 +81,9 @@ var DriveExport = common.Shortcut{ if strings.TrimSpace(spec.SubID) != "" { body["sub_id"] = spec.SubID } + if spec.OnlySchema { + body["only_schema"] = true + } dr := common.NewDryRunAPI(). Desc("3-step orchestration: create export task -> limited polling -> download file"). @@ -95,6 +101,7 @@ var DriveExport = common.Shortcut{ DocType: runtime.Str("doc-type"), FileExtension: runtime.Str("file-extension"), SubID: runtime.Str("sub-id"), + OnlySchema: runtime.Bool("only-schema"), } outputDir := runtime.Str("output-dir") preferredFileName := strings.TrimSpace(runtime.Str("file-name")) diff --git a/shortcuts/drive/drive_export_common.go b/shortcuts/drive/drive_export_common.go index 187ac3e73..94e9f625d 100644 --- a/shortcuts/drive/drive_export_common.go +++ b/shortcuts/drive/drive_export_common.go @@ -34,6 +34,7 @@ type driveExportSpec struct { DocType string FileExtension string SubID string + OnlySchema bool } // driveExportTaskResultCommand prints the resume command shown when bounded @@ -150,6 +151,10 @@ func validateDriveExportSpec(spec driveExportSpec) error { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--file-extension base only supports --doc-type bitable") } + if spec.OnlySchema && (spec.DocType != "bitable" || spec.FileExtension != "base") { + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--only-schema is only used when exporting bitable as base").WithParam("--only-schema") + } + if spec.FileExtension == "pptx" && spec.DocType != "slides" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--file-extension pptx only supports --doc-type slides") } @@ -185,6 +190,9 @@ func createDriveExportTask(runtime *common.RuntimeContext, spec driveExportSpec) if strings.TrimSpace(spec.SubID) != "" { body["sub_id"] = spec.SubID } + if spec.OnlySchema { + body["only_schema"] = true + } data, err := runtime.CallAPITyped("POST", "/open-apis/drive/v1/export_tasks", nil, body) if err != nil { diff --git a/shortcuts/drive/drive_export_test.go b/shortcuts/drive/drive_export_test.go index 619c9d43d..1899fd8a9 100644 --- a/shortcuts/drive/drive_export_test.go +++ b/shortcuts/drive/drive_export_test.go @@ -51,6 +51,15 @@ func TestValidateDriveExportSpec(t *testing.T) { name: "base bitable ok", spec: driveExportSpec{Token: "base123", DocType: "bitable", FileExtension: "base"}, }, + { + name: "base bitable only schema ok", + spec: driveExportSpec{Token: "base123", DocType: "bitable", FileExtension: "base", OnlySchema: true}, + }, + { + name: "only schema non base rejected", + spec: driveExportSpec{Token: "base123", DocType: "bitable", FileExtension: "xlsx", OnlySchema: true}, + wantErr: "--only-schema is only used", + }, { name: "slides pptx ok", spec: driveExportSpec{Token: "slides123", DocType: "slides", FileExtension: "pptx"}, @@ -612,6 +621,7 @@ func TestDriveExportBitableBaseAsyncSuccess(t *testing.T) { "--token", "bitable123", "--doc-type", "bitable", "--file-extension", "base", + "--only-schema", "--as", "bot", }, f, stdout) if err != nil { @@ -628,6 +638,9 @@ func TestDriveExportBitableBaseAsyncSuccess(t *testing.T) { if createBody["type"] != "bitable" { t.Fatalf("export_tasks body type = %v, want %q", createBody["type"], "bitable") } + if createBody["only_schema"] != true { + t.Fatalf("export_tasks body only_schema = %v, want true", createBody["only_schema"]) + } data, err := os.ReadFile(filepath.Join(tmpDir, "crm.base")) if err != nil { diff --git a/skills/lark-drive/references/lark-drive-export.md b/skills/lark-drive/references/lark-drive-export.md index a147915ec..47a261c43 100644 --- a/skills/lark-drive/references/lark-drive-export.md +++ b/skills/lark-drive/references/lark-drive-export.md @@ -76,6 +76,14 @@ lark-cli drive +export \ --file-extension base \ --output-dir ./exports +# 导出多维表格结构为 .base 快照(仅导出表结构,不导出记录数据) +lark-cli drive +export \ + --token "" \ + --doc-type bitable \ + --file-extension base \ + --only-schema \ + --output-dir ./exports + # 允许覆盖已存在文件 lark-cli drive +export \ --token "" \ @@ -92,6 +100,7 @@ lark-cli drive +export \ | `--doc-type` | 是 | 源文档类型:`doc` / `docx` / `sheet` / `bitable` / `slides` | | `--file-extension` | 是 | 导出格式:`docx` / `pdf` / `xlsx` / `csv` / `markdown` / `base` / `pptx` | | `--sub-id` | 条件必填 | 当 `sheet` / `bitable` 导出为 `csv` 时必填 | +| `--only-schema` | 否 | 仅当 `--doc-type bitable --file-extension base` 时可用;只导出多维表格结构,不导出记录数据 | | `--file-name` | 否 | 覆盖默认本地文件名;如未带扩展名,会按 `--file-extension` 自动补齐 | | `--output-dir` | 否 | 本地输出目录,默认当前目录 | | `--overwrite` | 否 | 覆盖已存在文件 | @@ -100,6 +109,7 @@ lark-cli drive +export \ - `markdown` 只支持 `docx` - `base` 只支持 `bitable` +- `--only-schema` 只支持 `bitable` 导出为 `.base`,用于仅导出表结构 - `pptx` 只支持 `slides` - `slides` 支持导出为 `pptx` / `pdf` - `sheet` / `bitable` 导出为 `csv` 时必须带 `--sub-id` diff --git a/tests/cli_e2e/drive/coverage.md b/tests/cli_e2e/drive/coverage.md index 1da102998..a3b38fe03 100644 --- a/tests/cli_e2e/drive/coverage.md +++ b/tests/cli_e2e/drive/coverage.md @@ -14,7 +14,7 @@ - TestDriveAddCommentDryRun_File: dry-run coverage for `drive +add-comment` on supported Drive file targets; pins the `metas.batch_query -> files/:token/new_comments` request chain, `file_type=file`, and the required placeholder `anchor.block_id`. - TestDriveAddCommentMarkdownFileWorkflow: opt-in live workflow skeleton for the same path, gated by `LARK_DRIVE_MD_COMMENT_E2E=1`. - TestDrive_SecureLabelDryRun: dry-run coverage for `drive +secure-label-list` and `drive +secure-label-update`; asserts label-list query params and update URL→type inference, request method/URL/type query, and `label-id` body shape. Runs without hitting live APIs because update can trigger document-level security approval flows. -- TestDriveExportDryRun_FileNameMetadata: dry-run coverage for `drive +export`; asserts export task request shape and local `--file-name` / `--output-dir` metadata without calling live APIs. +- TestDriveExportDryRun_FileNameMetadata / TestDriveExportDryRun_BitableBaseOnlySchema: dry-run coverage for `drive +export`; asserts export task request shape, local `--file-name` / `--output-dir` metadata, and `bitable` `.base` `only_schema` request body without calling live APIs. - TestDrive_PullDryRun / TestDrive_PullDryRunAcceptsDuplicateRemoteStrategies: dry-run coverage for `drive +pull`; asserts the list-files request shape, Validate-stage safety guards, and acceptance of `--on-duplicate-remote=rename|newest|oldest` by the real CLI binary. - TestDrive_PushDryRun / TestDrive_PushDryRunAcceptsDuplicateRemoteStrategies: dry-run coverage for `drive +push`; asserts the list-files request shape, Validate-stage safety guards, conditional delete preflight, and acceptance of `--on-duplicate-remote=newest|oldest` by the real CLI binary. - Cleanup note: `drive files delete` is only exercised in cleanup and is intentionally left uncovered. @@ -29,7 +29,7 @@ | ✓ | drive +apply-permission | shortcut | drive_apply_permission_dryrun_test.go::TestDrive_ApplyPermissionDryRun | `--token` URL vs bare; `--type` (enum) with URL inference; `--perm view\|edit`; `--remark` optional | dry-run only; no live-apply E2E because a real request pushes a card to the owner | | ✕ | drive +delete | shortcut | | none | no primary delete workflow yet | | ✕ | drive +download | shortcut | | none | no file fixture workflow yet | -| ✓ | drive +export | shortcut | drive_export_dryrun_test.go::TestDriveExportDryRun_FileNameMetadata | `--token`; `--doc-type`; `--file-extension`; `--file-name`; `--output-dir` | dry-run only; no live export workflow yet | +| ✓ | drive +export | shortcut | drive_export_dryrun_test.go::TestDriveExportDryRun_FileNameMetadata + TestDriveExportDryRun_BitableBaseOnlySchema | `--token`; `--doc-type`; `--file-extension`; `--file-name`; `--output-dir`; `--only-schema` | dry-run only; no live export workflow yet | | ✕ | drive +export-download | shortcut | | none | no export-download workflow yet | | ✕ | drive +import | shortcut | | none | no import workflow yet | | ✕ | drive +move | shortcut | | none | no move workflow yet | diff --git a/tests/cli_e2e/drive/drive_export_dryrun_test.go b/tests/cli_e2e/drive/drive_export_dryrun_test.go index 1d903d452..b4cb7d2b3 100644 --- a/tests/cli_e2e/drive/drive_export_dryrun_test.go +++ b/tests/cli_e2e/drive/drive_export_dryrun_test.go @@ -99,3 +99,44 @@ func TestDriveExportDryRun_MarkdownFetchAPI(t *testing.T) { t.Fatalf("output_dir=%q, want ./md-exports\nstdout:\n%s", got, out) } } + +func TestDriveExportDryRun_BitableBaseOnlySchema(t *testing.T) { + setDriveDryRunConfigEnv(t) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + t.Cleanup(cancel) + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "drive", "+export", + "--token", "bitableDryRunExport", + "--doc-type", "bitable", + "--file-extension", "base", + "--only-schema", + "--dry-run", + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + + out := result.Stdout + if got := gjson.Get(out, "api.0.method").String(); got != "POST" { + t.Fatalf("method=%q, want POST\nstdout:\n%s", got, out) + } + if got := gjson.Get(out, "api.0.url").String(); got != "/open-apis/drive/v1/export_tasks" { + t.Fatalf("url=%q, want export_tasks\nstdout:\n%s", got, out) + } + if got := gjson.Get(out, "api.0.body.token").String(); got != "bitableDryRunExport" { + t.Fatalf("body.token=%q, want bitableDryRunExport\nstdout:\n%s", got, out) + } + if got := gjson.Get(out, "api.0.body.type").String(); got != "bitable" { + t.Fatalf("body.type=%q, want bitable\nstdout:\n%s", got, out) + } + if got := gjson.Get(out, "api.0.body.file_extension").String(); got != "base" { + t.Fatalf("body.file_extension=%q, want base\nstdout:\n%s", got, out) + } + if got := gjson.Get(out, "api.0.body.only_schema").Bool(); !got { + t.Fatalf("body.only_schema=%v, want true\nstdout:\n%s", got, out) + } +}