diff --git a/internal/output/lark_errors.go b/internal/output/lark_errors.go index c5c5d10ba..e9c6fceb1 100644 --- a/internal/output/lark_errors.go +++ b/internal/output/lark_errors.go @@ -33,6 +33,11 @@ const ( LarkErrRefreshRevoked = 20064 // refresh_token revoked LarkErrRefreshAlreadyUsed = 20073 // refresh_token already consumed (single-use rotation) LarkErrRefreshServerError = 20050 // refresh endpoint server-side error, retryable + + // Drive shortcut / cross-space constraints. + LarkErrDriveResourceContention = 1061045 // resource contention occurred, please retry + LarkErrDriveCrossTenantUnit = 1064510 // cross tenant and unit not support + LarkErrDriveCrossBrand = 1064511 // cross brand not support ) // ClassifyLarkError maps a Lark API error code + message to (exitCode, errType, hint). @@ -60,6 +65,14 @@ func ClassifyLarkError(code int, msg string) (int, string, string) { // rate limit case LarkErrRateLimit: return ExitAPI, "rate_limit", "please try again later" + + // drive-specific constraints that benefit from actionable hints + case LarkErrDriveResourceContention: + return ExitAPI, "conflict", "please retry later and avoid concurrent duplicate requests" + case LarkErrDriveCrossTenantUnit: + return ExitAPI, "cross_tenant_unit", "operate on source and target within the same tenant and region/unit" + case LarkErrDriveCrossBrand: + return ExitAPI, "cross_brand", "operate on source and target within the same brand environment" } return ExitAPI, "api_error", "" diff --git a/internal/output/lark_errors_test.go b/internal/output/lark_errors_test.go new file mode 100644 index 000000000..b9ae5569b --- /dev/null +++ b/internal/output/lark_errors_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package output + +import ( + "strings" + "testing" +) + +// TestClassifyLarkError_DriveCreateShortcutConstraints verifies known Drive shortcut errors map to actionable hints. +func TestClassifyLarkError_DriveCreateShortcutConstraints(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + code int + wantExitCode int + wantType string + wantHint string + }{ + { + name: "resource contention", + code: LarkErrDriveResourceContention, + wantExitCode: ExitAPI, + wantType: "conflict", + wantHint: "avoid concurrent duplicate requests", + }, + { + name: "cross tenant unit", + code: LarkErrDriveCrossTenantUnit, + wantExitCode: ExitAPI, + wantType: "cross_tenant_unit", + wantHint: "same tenant and region/unit", + }, + { + name: "cross brand", + code: LarkErrDriveCrossBrand, + wantExitCode: ExitAPI, + wantType: "cross_brand", + wantHint: "same brand environment", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + gotExitCode, gotType, gotHint := ClassifyLarkError(tt.code, "raw msg") + if gotExitCode != tt.wantExitCode { + t.Fatalf("exitCode=%d, want %d", gotExitCode, tt.wantExitCode) + } + if gotType != tt.wantType { + t.Fatalf("type=%q, want %q", gotType, tt.wantType) + } + if gotHint == "" { + t.Fatal("expected non-empty hint") + } + if !strings.Contains(gotHint, tt.wantHint) { + t.Fatalf("hint=%q, want substring %q", gotHint, tt.wantHint) + } + }) + } +} diff --git a/shortcuts/drive/drive_create_shortcut.go b/shortcuts/drive/drive_create_shortcut.go new file mode 100644 index 000000000..92d0c5dc5 --- /dev/null +++ b/shortcuts/drive/drive_create_shortcut.go @@ -0,0 +1,136 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package drive + +import ( + "context" + "fmt" + "strings" + + "github.com/larksuite/cli/internal/output" + "github.com/larksuite/cli/internal/validate" + "github.com/larksuite/cli/shortcuts/common" +) + +var driveCreateShortcutAllowedTypes = map[string]bool{ + "file": true, + "docx": true, + "bitable": true, + "doc": true, + "sheet": true, + "mindnote": true, + "slides": true, +} + +type driveCreateShortcutSpec struct { + FileToken string + FileType string + FolderToken string +} + +func newDriveCreateShortcutSpec(runtime *common.RuntimeContext) driveCreateShortcutSpec { + return driveCreateShortcutSpec{ + FileToken: strings.TrimSpace(runtime.Str("file-token")), + FileType: strings.ToLower(strings.TrimSpace(runtime.Str("type"))), + FolderToken: strings.TrimSpace(runtime.Str("folder-token")), + } +} + +// RequestBody builds the create_shortcut API payload from the shortcut spec. +func (s driveCreateShortcutSpec) RequestBody() map[string]interface{} { + return map[string]interface{}{ + "parent_token": s.FolderToken, + "refer_entity": map[string]interface{}{ + "refer_token": s.FileToken, + "refer_type": s.FileType, + }, + } +} + +// DriveCreateShortcut creates a Drive shortcut for an existing file in another folder. +var DriveCreateShortcut = common.Shortcut{ + Service: "drive", + Command: "+create-shortcut", + Description: "Create a Drive shortcut in another folder", + Risk: "write", + Scopes: []string{"space:document:shortcut"}, + AuthTypes: []string{"user", "bot"}, + Flags: []common.Flag{ + {Name: "file-token", Desc: "source file token to reference", Required: true}, + {Name: "type", Desc: "source file type (file, docx, bitable, doc, sheet, mindnote, slides)", Required: true}, + {Name: "folder-token", Desc: "target folder token for the new shortcut", Required: true}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + return validateDriveCreateShortcutSpec(newDriveCreateShortcutSpec(runtime)) + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + spec := newDriveCreateShortcutSpec(runtime) + + return common.NewDryRunAPI(). + Desc("Create a Drive shortcut"). + POST("/open-apis/drive/v1/files/create_shortcut"). + Desc("[1] Create shortcut"). + Body(spec.RequestBody()) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + spec := newDriveCreateShortcutSpec(runtime) + + fmt.Fprintf( + runtime.IO().ErrOut, + "Creating shortcut for %s %s in folder %s...\n", + spec.FileType, + common.MaskToken(spec.FileToken), + common.MaskToken(spec.FolderToken), + ) + + data, err := runtime.CallAPI( + "POST", + "/open-apis/drive/v1/files/create_shortcut", + nil, + spec.RequestBody(), + ) + if err != nil { + return err + } + + out := map[string]interface{}{ + "created": true, + "source_file_token": spec.FileToken, + "source_type": spec.FileType, + "folder_token": spec.FolderToken, + } + if shortcutToken := common.GetString(data, "succ_shortcut_node", "token"); shortcutToken != "" { + out["shortcut_token"] = shortcutToken + } + if url := common.GetString(data, "succ_shortcut_node", "url"); url != "" { + out["url"] = url + } + if title := common.GetString(data, "succ_shortcut_node", "name"); title != "" { + out["title"] = title + } + + runtime.Out(out, nil) + return nil + }, +} + +// validateDriveCreateShortcutSpec validates shortcut creation inputs before API execution. +func validateDriveCreateShortcutSpec(spec driveCreateShortcutSpec) error { + if err := validate.ResourceName(spec.FileToken, "--file-token"); err != nil { + return output.ErrValidation("%s", err) + } + if err := validate.ResourceName(spec.FolderToken, "--folder-token"); err != nil { + return output.ErrValidation("%s", err) + } + if spec.FileType == "wiki" { + return output.ErrValidation("unsupported file type: wiki. This shortcut only supports Drive file tokens; wiki documents must be resolved to their underlying file token first") + } + if spec.FileType == "folder" { + return output.ErrValidation("unsupported file type: folder. The create_shortcut API only supports Drive files, not folders") + } + if !driveCreateShortcutAllowedTypes[spec.FileType] { + return output.ErrValidation("unsupported file type: %s. Supported types: file, docx, bitable, doc, sheet, mindnote, slides", spec.FileType) + } + return nil +} diff --git a/shortcuts/drive/drive_create_shortcut_test.go b/shortcuts/drive/drive_create_shortcut_test.go new file mode 100644 index 000000000..3d883b964 --- /dev/null +++ b/shortcuts/drive/drive_create_shortcut_test.go @@ -0,0 +1,336 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package drive + +import ( + "context" + "encoding/json" + "errors" + "strings" + "testing" + + "github.com/spf13/cobra" + + "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/internal/httpmock" + "github.com/larksuite/cli/internal/output" + "github.com/larksuite/cli/shortcuts/common" +) + +// TestValidateDriveCreateShortcutSpecRejectsUnsupportedTypes verifies unsupported source types are rejected early. +func TestValidateDriveCreateShortcutSpecRejectsUnsupportedTypes(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + spec driveCreateShortcutSpec + wantErr string + }{ + { + name: "wiki", + spec: driveCreateShortcutSpec{ + FileToken: "wiki_token_test", + FileType: "wiki", + FolderToken: "target_folder_token_test", + }, + wantErr: "underlying file token first", + }, + { + name: "folder", + spec: driveCreateShortcutSpec{ + FileToken: "folder_token_test", + FileType: "folder", + FolderToken: "target_folder_token_test", + }, + wantErr: "not folders", + }, + { + name: "shortcut", + spec: driveCreateShortcutSpec{ + FileToken: "shortcut_token_test", + FileType: "shortcut", + FolderToken: "target_folder_token_test", + }, + wantErr: "Supported types", + }, + { + name: "missing folder token", + spec: driveCreateShortcutSpec{ + FileToken: "file_token_test", + FileType: "docx", + }, + wantErr: "--folder-token must not be empty", + }, + { + name: "unknown", + spec: driveCreateShortcutSpec{ + FileToken: "file_token_test", + FileType: "unknown", + FolderToken: "target_folder_token_test", + }, + wantErr: "Supported types", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := validateDriveCreateShortcutSpec(tt.spec) + if err == nil { + t.Fatal("expected validation error, got nil") + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("error = %q, want substring %q", err.Error(), tt.wantErr) + } + }) + } +} + +// TestDriveCreateShortcutDryRunIncludesSingleCreateRequest verifies dry-run only previews the create request. +func TestDriveCreateShortcutDryRunIncludesSingleCreateRequest(t *testing.T) { + t.Parallel() + + cmd := &cobra.Command{Use: "drive +create-shortcut"} + cmd.Flags().String("file-token", "", "") + cmd.Flags().String("type", "", "") + cmd.Flags().String("folder-token", "", "") + if err := cmd.Flags().Set("file-token", " doc_token_test "); err != nil { + t.Fatalf("set --file-token: %v", err) + } + if err := cmd.Flags().Set("type", " DOCX "); err != nil { + t.Fatalf("set --type: %v", err) + } + if err := cmd.Flags().Set("folder-token", " folder_target_token_test "); err != nil { + t.Fatalf("set --folder-token: %v", err) + } + + runtime := common.TestNewRuntimeContext(cmd, nil) + dry := DriveCreateShortcut.DryRun(context.Background(), runtime) + if dry == nil { + t.Fatal("DryRun returned nil") + } + + data, err := json.Marshal(dry) + if err != nil { + t.Fatalf("marshal dry run: %v", err) + } + + var got struct { + API []struct { + Method string `json:"method"` + Body map[string]interface{} `json:"body"` + } `json:"api"` + } + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal dry run json: %v", err) + } + if len(got.API) != 1 { + t.Fatalf("expected 1 API call, got %d", len(got.API)) + } + if got.API[0].Method != "POST" { + t.Fatalf("first method = %q, want POST", got.API[0].Method) + } + if got.API[0].Body["parent_token"] != "folder_target_token_test" { + t.Fatalf("parent_token = %#v, want folder_target_token_test", got.API[0].Body["parent_token"]) + } + referEntity, _ := got.API[0].Body["refer_entity"].(map[string]interface{}) + if referEntity["refer_token"] != "doc_token_test" || referEntity["refer_type"] != "docx" { + t.Fatalf("unexpected refer_entity: %#v", referEntity) + } +} + +// TestDriveCreateShortcutUsesProvidedFolderToken verifies execution uses the explicit target folder token. +func TestDriveCreateShortcutUsesProvidedFolderToken(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig()) + createStub := &httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/files/create_shortcut", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "succ_shortcut_node": map[string]interface{}{ + "token": "shortcut_token_test", + "name": "shortcut_name_test", + "type": "docx", + "parent_token": "folder_target_token_test", + "url": "https://example.feishu.cn/docx/shortcut_token_test", + "shortcut_info": map[string]interface{}{ + "target_type": "docx", + "target_token": "doc_token_test", + }, + }, + }, + }, + } + reg.Register(createStub) + + err := mountAndRunDrive(t, DriveCreateShortcut, []string{ + "+create-shortcut", + "--file-token", " doc_token_test ", + "--type", " DOCX ", + "--folder-token", " folder_target_token_test ", + "--as", "bot", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + body := decodeCapturedJSONBody(t, createStub) + if body["parent_token"] != "folder_target_token_test" { + t.Fatalf("parent_token = %#v, want folder_target_token_test", body["parent_token"]) + } + referEntity, _ := body["refer_entity"].(map[string]interface{}) + if referEntity["refer_token"] != "doc_token_test" || referEntity["refer_type"] != "docx" { + t.Fatalf("unexpected refer_entity: %#v", referEntity) + } + + data := decodeDriveEnvelope(t, stdout) + if data["shortcut_token"] != "shortcut_token_test" { + t.Fatalf("shortcut_token = %#v, want shortcut_token_test", data["shortcut_token"]) + } + if data["folder_token"] != "folder_target_token_test" { + t.Fatalf("folder_token = %#v, want folder_target_token_test", data["folder_token"]) + } + if data["source_file_token"] != "doc_token_test" { + t.Fatalf("source_file_token = %#v, want doc_token_test", data["source_file_token"]) + } + if data["title"] != "shortcut_name_test" { + t.Fatalf("title = %#v, want shortcut_name_test", data["title"]) + } + if data["url"] != "https://example.feishu.cn/docx/shortcut_token_test" { + t.Fatalf("url = %#v, want https://example.feishu.cn/docx/shortcut_token_test", data["url"]) + } + if data["created"] != true { + t.Fatalf("created = %#v, want true", data["created"]) + } +} + +// TestDriveCreateShortcutValidateRequiresFolderToken verifies folder-token is mandatory. +func TestDriveCreateShortcutValidateRequiresFolderToken(t *testing.T) { + err := validateDriveCreateShortcutSpec(driveCreateShortcutSpec{ + FileToken: "doc_token_test", + FileType: "docx", + }) + if err == nil { + t.Fatal("expected validation error, got nil") + } + if !strings.Contains(err.Error(), "--folder-token must not be empty") { + t.Fatalf("unexpected error: %v", err) + } +} + +// TestDriveCreateShortcutValidateRejectsWhitespaceOnlyFolderToken verifies runtime normalization rejects blank folder tokens. +func TestDriveCreateShortcutValidateRejectsWhitespaceOnlyFolderToken(t *testing.T) { + t.Parallel() + + cmd := &cobra.Command{Use: "drive +create-shortcut"} + cmd.Flags().String("file-token", "", "") + cmd.Flags().String("type", "", "") + cmd.Flags().String("folder-token", "", "") + if err := cmd.Flags().Set("file-token", "doc_token_test"); err != nil { + t.Fatalf("set --file-token: %v", err) + } + if err := cmd.Flags().Set("type", " DOCX "); err != nil { + t.Fatalf("set --type: %v", err) + } + if err := cmd.Flags().Set("folder-token", " "); err != nil { + t.Fatalf("set --folder-token: %v", err) + } + + runtime := common.TestNewRuntimeContext(cmd, nil) + err := DriveCreateShortcut.Validate(context.Background(), runtime) + if err == nil { + t.Fatal("expected validation error, got nil") + } + if !strings.Contains(err.Error(), "--folder-token must not be empty") { + t.Fatalf("unexpected error: %v", err) + } +} + +// TestDriveCreateShortcutClassifiesKnownAPIConstraints verifies known API constraints surface as structured errors. +func TestDriveCreateShortcutClassifiesKnownAPIConstraints(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + code int + msg string + wantType string + wantHint string + wantMsgPart string + }{ + { + name: "resource contention", + code: output.LarkErrDriveResourceContention, + msg: "resource contention occurred, please retry", + wantType: "conflict", + wantHint: "avoid concurrent duplicate requests", + wantMsgPart: "resource contention occurred", + }, + { + name: "cross tenant and unit", + code: output.LarkErrDriveCrossTenantUnit, + msg: "cross tenant and unit not support", + wantType: "cross_tenant_unit", + wantHint: "same tenant and region/unit", + wantMsgPart: "cross tenant and unit not support", + }, + { + name: "cross brand", + code: output.LarkErrDriveCrossBrand, + msg: "cross brand not support", + wantType: "cross_brand", + wantHint: "same brand environment", + wantMsgPart: "cross brand not support", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + f, _, _, reg := cmdutil.TestFactory(t, driveTestConfig()) + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/drive/v1/files/create_shortcut", + Body: map[string]interface{}{ + "code": float64(tt.code), + "msg": tt.msg, + }, + }) + + err := mountAndRunDrive(t, DriveCreateShortcut, []string{ + "+create-shortcut", + "--file-token", "doc_token_test", + "--type", "docx", + "--folder-token", "folder_token_test", + "--as", "bot", + }, f, nil) + if err == nil { + t.Fatal("expected API error, got nil") + } + + var exitErr *output.ExitError + if !errors.As(err, &exitErr) || exitErr.Detail == nil { + t.Fatalf("expected structured exit error, got %v", err) + } + if exitErr.Code != output.ExitAPI { + t.Fatalf("exit code = %d, want %d", exitErr.Code, output.ExitAPI) + } + if exitErr.Detail.Type != tt.wantType { + t.Fatalf("type = %q, want %q", exitErr.Detail.Type, tt.wantType) + } + if exitErr.Detail.Code != tt.code { + t.Fatalf("detail code = %d, want %d", exitErr.Detail.Code, tt.code) + } + if !strings.Contains(exitErr.Detail.Message, tt.wantMsgPart) { + t.Fatalf("message = %q, want substring %q", exitErr.Detail.Message, tt.wantMsgPart) + } + if !strings.Contains(exitErr.Detail.Hint, tt.wantHint) { + t.Fatalf("hint = %q, want substring %q", exitErr.Detail.Hint, tt.wantHint) + } + }) + } +} diff --git a/shortcuts/drive/shortcuts.go b/shortcuts/drive/shortcuts.go index 55c5e07b4..1302f1f60 100644 --- a/shortcuts/drive/shortcuts.go +++ b/shortcuts/drive/shortcuts.go @@ -9,6 +9,7 @@ import "github.com/larksuite/cli/shortcuts/common" func Shortcuts() []common.Shortcut { return []common.Shortcut{ DriveUpload, + DriveCreateShortcut, DriveDownload, DriveAddComment, DriveExport, diff --git a/shortcuts/drive/shortcuts_test.go b/shortcuts/drive/shortcuts_test.go index fb2248ddb..cfdbc9288 100644 --- a/shortcuts/drive/shortcuts_test.go +++ b/shortcuts/drive/shortcuts_test.go @@ -5,12 +5,14 @@ package drive import "testing" +// TestShortcutsIncludesExpectedCommands verifies the drive shortcut registry contains the expected commands. func TestShortcutsIncludesExpectedCommands(t *testing.T) { t.Parallel() got := Shortcuts() want := []string{ "+upload", + "+create-shortcut", "+download", "+add-comment", "+export", diff --git a/skills/lark-drive/SKILL.md b/skills/lark-drive/SKILL.md index a47ed0ef0..265afec8c 100644 --- a/skills/lark-drive/SKILL.md +++ b/skills/lark-drive/SKILL.md @@ -194,6 +194,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli drive + [flags]`) |----------|------| | [`+upload`](references/lark-drive-upload.md) | Upload a local file to Drive | | [`+download`](references/lark-drive-download.md) | Download a file from Drive to local | +| [`+create-shortcut`](references/lark-drive-create-shortcut.md) | Create a shortcut to an existing Drive file in another folder | | [`+add-comment`](references/lark-drive-add-comment.md) | Add a full-document comment, or a local comment to selected docx text (also supports wiki URL resolving to doc/docx) | | [`+export`](references/lark-drive-export.md) | Export a doc/docx/sheet/bitable to a local file with limited polling | | [`+export-download`](references/lark-drive-export-download.md) | Download an exported file by file_token | diff --git a/skills/lark-drive/references/lark-drive-create-shortcut.md b/skills/lark-drive/references/lark-drive-create-shortcut.md new file mode 100644 index 000000000..f2a892b64 --- /dev/null +++ b/skills/lark-drive/references/lark-drive-create-shortcut.md @@ -0,0 +1,103 @@ + +# drive +create-shortcut + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 + +在目标文件夹中为一个现有 Drive 文件创建快捷方式。 + +## 命令 + +```bash +# 为普通文件创建快捷方式 +lark-cli drive +create-shortcut \ + --folder-token \ + --file-token \ + --type file + +# 为新版文档创建快捷方式 +lark-cli drive +create-shortcut \ + --folder-token \ + --file-token \ + --type docx + +# 为电子表格创建快捷方式 +lark-cli drive +create-shortcut \ + --folder-token \ + --file-token \ + --type sheet + +# 仅预览即将发起的请求,不真正执行 +lark-cli drive +create-shortcut \ + --folder-token \ + --file-token \ + --type docx \ + --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--folder-token` | 是 | 目标父文件夹 token | +| `--file-token` | 是 | 源文件 token,表示被引用的原始文件 | +| `--type` | 是 | 源文件类型,推荐值:`file`、`docx`、`doc`、`sheet`、`bitable`、`mindnote`、`slides` | + +## 输入规则 + +- 该 shortcut 的最小输入是 `--folder-token` + `--file-token` + `--type` +- CLI 层会把 `--file-token` 和 `--type` 组装为底层 API 所需的 `refer_entity` +- `--file-token` 必须是 Drive 文件 token,不要直接传 wiki 节点 token +- 如果来源是 `/wiki/...` 链接,必须先按 [`lark-drive`](../SKILL.md) 中的 wiki 解析流程拿到真实 `obj_token`,再创建快捷方式 +- 目标位置必须是云空间文件夹;这个 shortcut 不是“复制文件内容”,而是“在另一个文件夹里挂一个引用入口” + +## 类型说明 + +| 类型 | 说明 | +|------|------| +| `file` | 普通文件 | +| `docx` | 新版云文档 | +| `doc` | 旧版云文档 | +| `sheet` | 电子表格 | +| `bitable` | 多维表格 | +| `mindnote` | 思维笔记 | +| `slides` | 幻灯片 | + +## 行为说明 + +- 成功时会调用 `POST /open-apis/drive/v1/files/create_shortcut` +- 该 shortcut 继承通用能力,可配合 `--as user|bot|auto`、`--format`、`--jq`、`--dry-run` 使用 +- `--dry-run` 只输出请求方法、路径、身份和请求体预览,不会真正创建快捷方式 +- 这是写入操作;执行前应确认目标文件夹和源文件都准确无误 + +## 限制 + +- 该接口不支持并发调用 +- 调用频率上限为 5 QPS,且 10000 次/天 +- 不支持跨租户、跨地域创建快捷方式 +- 不支持跨品牌创建快捷方式 +- 如果目标父文件夹单层挂载数量超过限制,会返回 `1062507` + +## 权限要求 + +- 当前调用身份需要能访问源文件 +- 当前调用身份需要对目标文件夹有编辑权限 +- 如果权限不足,常见表现为 `1061004 forbidden` + +## 常见错误 + +| 错误码 / 错误信息 | 原因 | 处理建议 | +|------|------|------| +| `1061002 params error` | 缺少必填参数,或 `--file-token` / `--type` 组合无法构成有效源文件信息 | 检查 `--file-token`、`--type` 是否完整且匹配;如显式传了 `--folder-token`,再确认其值有效 | +| `1061003 not found` | 源文件或目标文件夹不存在 | 重新确认 token 是否正确、资源是否已删除 | +| `1061004 forbidden` | 对源文件没有访问权限,或对目标文件夹没有编辑权限 | 切换到有权限的身份,或先授予文档 / 文件夹权限 | +| `1061005 auth failed` | 身份类型或 access token 不正确 | 检查 `--as` 使用的身份及当前登录态 | +| `1061007 file has been delete` | 源文件已删除 | 确认原文件仍存在,再重新执行 | +| `1062507 parent node out of sibling num` | 目标文件夹单层挂载数超过上限 | 清理目标目录,或换一个父文件夹 | +| `1061045 resource contention occurred, please retry` | 平台内部资源争抢 | 稍后重试,不要并发重复调用 | +| `1064510 cross tenant and unit not support` | 跨租户或跨地域请求 | 改为在同租户、同地域范围内操作 | +| `1064511 cross brand not support` | 跨品牌请求 | 改为在同品牌环境内操作 | + +## 参考 + +- [lark-drive](../SKILL.md) -- 云空间全部命令 +- [lark-shared](../../lark-shared/SKILL.md) -- 认证和全局参数