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
13 changes: 13 additions & 0 deletions internal/output/lark_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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", ""
Expand Down
64 changes: 64 additions & 0 deletions internal/output/lark_errors_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
136 changes: 136 additions & 0 deletions shortcuts/drive/drive_create_shortcut.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading