Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
var DriveAddComment = common.Shortcut{ Service: "drive", Command: "+add-comment", Description: "Add a full-document comment, or a local comment to selected docx text (also supports wiki URL resolving to doc/docx)", Risk: "write", Scopes: []string{ "docx:document:readonly", "docs:document.comment:create", "docs:document.comment:write_only", }, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "doc", Desc: "document URL/token, or wiki URL that resolves to doc/docx", Required: true}, {Name: "content", Desc: "reply_elements JSON string", Required: true}, {Name: "full-comment", Type: "bool", Desc: "create a full-document comment; also the default when no location is provided"}, {Name: "selection-with-ellipsis", Desc: "target content locator (plain text or 'start...end')"}, {Name: "block-id", Desc: "anchor block ID (skip MCP locate-doc if already known)"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { docRef, err := parseCommentDocRef(runtime.Str("doc")) if err != nil { return err } if _, err := parseCommentReplyElements(runtime.Str("content")); err != nil { return err } selection := runtime.Str("selection-with-ellipsis") blockID := strings.TrimSpace(runtime.Str("block-id")) if strings.TrimSpace(selection) != "" && blockID != "" { return output.ErrValidation("--selection-with-ellipsis and --block-id are mutually exclusive") } if runtime.Bool("full-comment") && (strings.TrimSpace(selection) != "" || blockID != "") { return output.ErrValidation("--full-comment cannot be used with --selection-with-ellipsis or --block-id") } mode := resolveCommentMode(runtime.Bool("full-comment"), selection, blockID) if mode == commentModeLocal && docRef.Kind == "doc" { return output.ErrValidation("local comments only support docx documents; use --full-comment or omit location flags for a whole-document comment") } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { docRef, _ := parseCommentDocRef(runtime.Str("doc")) replyElements, _ := parseCommentReplyElements(runtime.Str("content")) selection := runtime.Str("selection-with-ellipsis") blockID := strings.TrimSpace(runtime.Str("block-id")) mode := resolveCommentMode(runtime.Bool("full-comment"), selection, blockID) targetToken, targetFileType, resolvedBy := dryRunResolvedCommentTarget(docRef, mode) createPath := "/open-apis/drive/v1/files/:file_token/new_comments" commentBody := buildCommentCreateV2Request(targetFileType, "", replyElements) if mode == commentModeLocal { commentBody = buildCommentCreateV2Request(targetFileType, anchorBlockIDForDryRun(blockID), replyElements) } mcpEndpoint := common.MCPEndpoint(runtime.Config.Brand) dry := common.NewDryRunAPI() switch { case mode == commentModeFull && resolvedBy == "wiki": dry.Desc("2-step orchestration: resolve wiki -> create full comment") case mode == commentModeFull: dry.Desc("1-step request: create full comment") case resolvedBy == "wiki" && strings.TrimSpace(selection) != "": dry.Desc("3-step orchestration: resolve wiki -> locate block -> create local comment") case resolvedBy == "wiki": dry.Desc("2-step orchestration: resolve wiki -> create local comment") case strings.TrimSpace(selection) != "": dry.Desc("2-step orchestration: locate block -> create local comment") default: dry.Desc("1-step request: create local comment with explicit block ID") } if resolvedBy == "wiki" { dry.GET("/open-apis/wiki/v2/spaces/get_node"). Desc("[1] Resolve wiki node to target document"). Params(map[string]interface{}{"token": docRef.Token}) } if mode == commentModeLocal && strings.TrimSpace(selection) != "" { step := "[1]" if resolvedBy == "wiki" { step = "[2]" } mcpArgs := map[string]interface{}{ "doc_id": dryRunLocateDocRef(docRef), "limit": defaultLocateDocLimit, "selection_with_ellipsis": selection, } dry.POST(mcpEndpoint). Desc(step+" MCP tool: locate-doc"). Body(map[string]interface{}{ "method": "tools/call", "params": map[string]interface{}{ "name": "locate-doc", "arguments": mcpArgs, }, }). Set("mcp_tool", "locate-doc"). Set("args", mcpArgs) } step := "[1]" createDesc := "Create full comment" if mode == commentModeLocal { createDesc = "Create local comment" step = "[2]" if resolvedBy == "wiki" && strings.TrimSpace(selection) != "" { step = "[3]" } else if resolvedBy == "wiki" || strings.TrimSpace(selection) != "" { step = "[2]" } else { step = "[1]" } } else if resolvedBy == "wiki" { step = "[2]" } return dry.POST(createPath). Desc(step+" "+createDesc). Body(commentBody). Set("file_token", targetToken) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { selection := runtime.Str("selection-with-ellipsis") blockID := strings.TrimSpace(runtime.Str("block-id")) mode := resolveCommentMode(runtime.Bool("full-comment"), selection, blockID) target, err := resolveCommentTarget(ctx, runtime, runtime.Str("doc"), mode) if err != nil { return err } replyElements, err := parseCommentReplyElements(runtime.Str("content")) if err != nil { return err } var locateResult locateDocResult selectedMatch := 0 if mode == commentModeLocal && blockID == "" { _, locateResult, err = locateDocumentSelection(runtime, target, selection, defaultLocateDocLimit) if err != nil { return err } match, idx, err := selectLocateMatch(locateResult) if err != nil { return err } blockID = match.AnchorBlockID if strings.TrimSpace(blockID) == "" { return output.Errorf(output.ExitAPI, "api_error", "locate-doc response missing anchor_block_id") } selectedMatch = idx fmt.Fprintf(runtime.IO().ErrOut, "Locate-doc matched %d block(s); using match #%d (%s)\n", len(locateResult.Matches), idx, blockID) } else if mode == commentModeLocal { fmt.Fprintf(runtime.IO().ErrOut, "Using explicit block ID: %s\n", blockID) } requestPath := fmt.Sprintf("/open-apis/drive/v1/files/%s/new_comments", validate.EncodePathSegment(target.FileToken)) requestBody := buildCommentCreateV2Request(target.FileType, "", replyElements) if mode == commentModeLocal { requestBody = buildCommentCreateV2Request(target.FileType, blockID, replyElements) } if mode == commentModeLocal { fmt.Fprintf(runtime.IO().ErrOut, "Creating local comment in %s\n", common.MaskToken(target.FileToken)) } else { fmt.Fprintf(runtime.IO().ErrOut, "Creating full comment in %s\n", common.MaskToken(target.FileToken)) } data, err := runtime.CallAPI( "POST", requestPath, nil, requestBody, ) if err != nil { return err } out := map[string]interface{}{ "comment_id": data["comment_id"], "doc_id": target.DocID, "file_token": target.FileToken, "file_type": target.FileType, "resolved_by": target.ResolvedBy, "comment_mode": string(mode), } if createdAt := firstPresentValue(data, "created_at", "create_time"); createdAt != nil { out["created_at"] = createdAt } if target.WikiToken != "" { out["wiki_token"] = target.WikiToken } if mode == commentModeLocal { out["anchor_block_id"] = blockID out["selection_source"] = "block_id" if strings.TrimSpace(selection) != "" { out["selection_source"] = "locate-doc" out["selection_with_ellipsis"] = selection out["match_count"] = locateResult.MatchCount out["match_index"] = selectedMatch } } else if isWhole, ok := data["is_whole"]; ok { out["is_whole"] = isWhole } runtime.Out(out, nil) return nil }, }
var DriveDownload = common.Shortcut{ Service: "drive", Command: "+download", Description: "Download a file from Drive to local", Risk: "read", Scopes: []string{"drive:file:download"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "file-token", Desc: "file token", Required: true}, {Name: "output", Desc: "local save path"}, {Name: "overwrite", Type: "bool", Desc: "overwrite existing output file"}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { fileToken := runtime.Str("file-token") outputPath := runtime.Str("output") if outputPath == "" { outputPath = fileToken } return common.NewDryRunAPI(). GET("/open-apis/drive/v1/files/:file_token/download"). Set("file_token", fileToken).Set("output", outputPath) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { fileToken := runtime.Str("file-token") outputPath := runtime.Str("output") overwrite := runtime.Bool("overwrite") if err := validate.ResourceName(fileToken, "--file-token"); err != nil { return output.ErrValidation("%s", err) } if outputPath == "" { outputPath = fileToken } safePath, err := validate.SafeOutputPath(outputPath) if err != nil { return output.ErrValidation("unsafe output path: %s", err) } if err := common.EnsureWritableFile(safePath, overwrite); err != nil { return err } fmt.Fprintf(runtime.IO().ErrOut, "Downloading: %s\n", common.MaskToken(fileToken)) resp, err := runtime.DoAPIStream(ctx, &larkcore.ApiReq{ HttpMethod: http.MethodGet, ApiPath: fmt.Sprintf("/open-apis/drive/v1/files/%s/download", validate.EncodePathSegment(fileToken)), }) if err != nil { return output.ErrNetwork("download failed: %s", err) } defer resp.Body.Close() if err := vfs.MkdirAll(filepath.Dir(safePath), 0700); err != nil { return output.Errorf(output.ExitInternal, "api_error", "cannot create parent directory: %s", err) } sizeBytes, err := validate.AtomicWriteFromReader(safePath, resp.Body, 0600) if err != nil { return output.Errorf(output.ExitInternal, "api_error", "cannot create file: %s", err) } runtime.Out(map[string]interface{}{ "saved_path": safePath, "size_bytes": sizeBytes, }, nil) return nil }, }
var DriveExport = common.Shortcut{ Service: "drive", Command: "+export", Description: "Export a doc/docx/sheet/bitable to a local file with limited polling", Risk: "read", Scopes: []string{ "docs:document.content:read", "docs:document:export", "drive:drive.metadata:readonly", }, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "token", Desc: "source document token", Required: true}, {Name: "doc-type", Desc: "source document type: doc | docx | sheet | bitable", Required: true, Enum: []string{"doc", "docx", "sheet", "bitable"}}, {Name: "file-extension", Desc: "export format: docx | pdf | xlsx | csv | markdown", Required: true, Enum: []string{"docx", "pdf", "xlsx", "csv", "markdown"}}, {Name: "sub-id", Desc: "sub-table/sheet ID, required when exporting sheet/bitable as csv"}, {Name: "output-dir", Default: ".", Desc: "local output directory (default: current directory)"}, {Name: "overwrite", Type: "bool", Desc: "overwrite existing output file"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateDriveExportSpec(driveExportSpec{ Token: runtime.Str("token"), DocType: runtime.Str("doc-type"), FileExtension: runtime.Str("file-extension"), SubID: runtime.Str("sub-id"), }) }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { spec := driveExportSpec{ Token: runtime.Str("token"), DocType: runtime.Str("doc-type"), FileExtension: runtime.Str("file-extension"), SubID: runtime.Str("sub-id"), } if spec.FileExtension == "markdown" { return common.NewDryRunAPI(). Desc("2-step orchestration: fetch docx markdown -> write local file"). GET("/open-apis/docs/v1/content"). Params(map[string]interface{}{ "doc_token": spec.Token, "doc_type": "docx", "content_type": "markdown", }) } body := map[string]interface{}{ "token": spec.Token, "type": spec.DocType, "file_extension": spec.FileExtension, } if strings.TrimSpace(spec.SubID) != "" { body["sub_id"] = spec.SubID } return common.NewDryRunAPI(). Desc("3-step orchestration: create export task -> limited polling -> download file"). POST("/open-apis/drive/v1/export_tasks"). Body(body) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { spec := driveExportSpec{ Token: runtime.Str("token"), DocType: runtime.Str("doc-type"), FileExtension: runtime.Str("file-extension"), SubID: runtime.Str("sub-id"), } outputDir := runtime.Str("output-dir") overwrite := runtime.Bool("overwrite") if spec.FileExtension == "markdown" { fmt.Fprintf(runtime.IO().ErrOut, "Exporting docx as markdown: %s\n", common.MaskToken(spec.Token)) data, err := runtime.CallAPI( "GET", "/open-apis/docs/v1/content", map[string]interface{}{ "doc_token": spec.Token, "doc_type": "docx", "content_type": "markdown", }, nil, ) if err != nil { return err } title, err := fetchDriveMetaTitle(runtime, spec.Token, spec.DocType) if err != nil { fmt.Fprintf(runtime.IO().ErrOut, "Title lookup failed, using token as filename: %v\n", err) title = spec.Token } fileName := ensureExportFileExtension(sanitizeExportFileName(title, spec.Token), spec.FileExtension) savedPath, err := saveContentToOutputDir(outputDir, fileName, []byte(common.GetString(data, "content")), overwrite) if err != nil { return err } runtime.Out(map[string]interface{}{ "token": spec.Token, "doc_type": spec.DocType, "file_extension": spec.FileExtension, "file_name": filepath.Base(savedPath), "saved_path": savedPath, "size_bytes": len([]byte(common.GetString(data, "content"))), }, nil) return nil } ticket, err := createDriveExportTask(runtime, spec) if err != nil { return err } fmt.Fprintf(runtime.IO().ErrOut, "Created export task: %s\n", ticket) var lastStatus driveExportStatus var lastPollErr error hasObservedStatus := false for attempt := 1; attempt <= driveExportPollAttempts; attempt++ { if attempt > 1 { select { case <-ctx.Done(): return ctx.Err() case <-time.After(driveExportPollInterval): } } if err := ctx.Err(); err != nil { return err } status, err := getDriveExportStatus(runtime, spec.Token, ticket) if err != nil { lastPollErr = err fmt.Fprintf(runtime.IO().ErrOut, "Export status attempt %d/%d failed: %v\n", attempt, driveExportPollAttempts, err) continue } lastStatus = status hasObservedStatus = true if status.Ready() { fmt.Fprintf(runtime.IO().ErrOut, "Export task completed: %s\n", common.MaskToken(status.FileToken)) fileName := ensureExportFileExtension(sanitizeExportFileName(status.FileName, spec.Token), spec.FileExtension) out, err := downloadDriveExportFile(ctx, runtime, status.FileToken, outputDir, fileName, overwrite) if err != nil { recoveryCommand := driveExportDownloadCommand(status.FileToken, fileName, outputDir, overwrite) hint := fmt.Sprintf( "the export artifact is already ready (ticket=%s, file_token=%s)\nretry download with: %s", ticket, status.FileToken, recoveryCommand, ) var exitErr *output.ExitError if errors.As(err, &exitErr) && exitErr.Detail != nil { return output.ErrWithHint(exitErr.Code, exitErr.Detail.Type, exitErr.Detail.Message, hint) } return output.ErrWithHint(output.ExitAPI, "api_error", err.Error(), hint) } out["ticket"] = ticket out["doc_type"] = spec.DocType out["file_extension"] = spec.FileExtension runtime.Out(out, nil) return nil } if status.Failed() { msg := strings.TrimSpace(status.JobErrorMsg) if msg == "" { msg = status.StatusLabel() } return output.Errorf(output.ExitAPI, "api_error", "export task failed: %s (ticket=%s)", msg, ticket) } fmt.Fprintf(runtime.IO().ErrOut, "Export status %d/%d: %s\n", attempt, driveExportPollAttempts, status.StatusLabel()) } nextCommand := driveExportTaskResultCommand(ticket, spec.Token) if !hasObservedStatus && lastPollErr != nil { hint := fmt.Sprintf( "the export task was created but every status poll failed (ticket=%s)\nretry status lookup with: %s", ticket, nextCommand, ) var exitErr *output.ExitError if errors.As(lastPollErr, &exitErr) && exitErr.Detail != nil { if strings.TrimSpace(exitErr.Detail.Hint) != "" { hint = exitErr.Detail.Hint + "\n" + hint } return output.ErrWithHint(exitErr.Code, exitErr.Detail.Type, exitErr.Detail.Message, hint) } return output.ErrWithHint(output.ExitAPI, "api_error", lastPollErr.Error(), hint) } failed := false var jobStatus interface{} jobStatusLabel := "unknown" if hasObservedStatus { failed = lastStatus.Failed() jobStatus = lastStatus.JobStatus jobStatusLabel = lastStatus.StatusLabel() } runtime.Out(map[string]interface{}{ "ticket": ticket, "token": spec.Token, "doc_type": spec.DocType, "file_extension": spec.FileExtension, "ready": false, "failed": failed, "job_status": jobStatus, "job_status_label": jobStatusLabel, "timed_out": true, "next_command": nextCommand, }, nil) fmt.Fprintf(runtime.IO().ErrOut, "Export task is still in progress. Continue with: %s\n", nextCommand) return nil }, }
DriveExport exports Drive-native documents to local files and falls back to a follow-up command when the async export task does not finish in time.
var DriveExportDownload = common.Shortcut{ Service: "drive", Command: "+export-download", Description: "Download an exported file by file_token", Risk: "read", Scopes: []string{ "docs:document:export", }, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "file-token", Desc: "exported file token", Required: true}, {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"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := validate.ResourceName(runtime.Str("file-token"), "--file-token"); err != nil { return output.ErrValidation("%s", err) } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). GET("/open-apis/drive/v1/export_tasks/file/:file_token/download"). Set("file_token", runtime.Str("file-token")). Set("output_dir", runtime.Str("output-dir")) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { out, err := downloadDriveExportFile( ctx, runtime, runtime.Str("file-token"), runtime.Str("output-dir"), runtime.Str("file-name"), runtime.Bool("overwrite"), ) if err != nil { return err } runtime.Out(out, nil) return nil }, }
DriveExportDownload downloads an already-generated export artifact when the caller has a file token from a previous export task.
var DriveImport = common.Shortcut{ Service: "drive", Command: "+import", Description: "Import a local file to Drive as a cloud document (docx, sheet, bitable)", Risk: "write", Scopes: []string{ "docs:document.media:upload", "docs:document:import", }, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "file", Desc: "local file path (e.g. .docx, .xlsx, .md; large files auto use multipart upload)", Required: true}, {Name: "type", Desc: "target document type (docx, sheet, bitable)", Required: true}, {Name: "folder-token", Desc: "target folder token (omit for root folder; API accepts empty mount_key as root)"}, {Name: "name", Desc: "imported file name (default: local file name without extension)"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateDriveImportSpec(driveImportSpec{ FilePath: runtime.Str("file"), DocType: strings.ToLower(runtime.Str("type")), FolderToken: runtime.Str("folder-token"), Name: runtime.Str("name"), }) }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { spec := driveImportSpec{ FilePath: runtime.Str("file"), DocType: strings.ToLower(runtime.Str("type")), FolderToken: runtime.Str("folder-token"), Name: runtime.Str("name"), } fileSize, err := preflightDriveImportFile(&spec) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } dry := common.NewDryRunAPI() dry.Desc("Upload file (single-part or multipart) -> create import task -> poll status") appendDriveImportUploadDryRun(dry, spec, fileSize) dry.POST("/open-apis/drive/v1/import_tasks"). Desc("[2] Create import task"). Body(spec.CreateTaskBody("<file_token>")) dry.GET("/open-apis/drive/v1/import_tasks/:ticket"). Desc("[3] Poll import task result"). Set("ticket", "<ticket>") return dry }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { spec := driveImportSpec{ FilePath: runtime.Str("file"), DocType: strings.ToLower(runtime.Str("type")), FolderToken: runtime.Str("folder-token"), Name: runtime.Str("name"), } if _, err := preflightDriveImportFile(&spec); err != nil { return err } fileToken, uploadErr := uploadMediaForImport(ctx, runtime, spec.FilePath, spec.SourceFileName(), spec.DocType) if uploadErr != nil { return uploadErr } fmt.Fprintf(runtime.IO().ErrOut, "Creating import task for %s as %s...\n", spec.TargetFileName(), spec.DocType) ticket, err := createDriveImportTask(runtime, spec, fileToken) if err != nil { return err } fmt.Fprintf(runtime.IO().ErrOut, "Polling import task %s...\n", ticket) status, ready, err := pollDriveImportTask(runtime, ticket) if err != nil { return err } resultType := status.DocType if resultType == "" { resultType = spec.DocType } out := map[string]interface{}{ "ticket": ticket, "type": resultType, "ready": ready, "job_status": status.JobStatus, "job_status_label": status.StatusLabel(), } if status.Token != "" { out["token"] = status.Token } if status.URL != "" { out["url"] = status.URL } if status.JobErrorMsg != "" { out["job_error_msg"] = status.JobErrorMsg } if status.Extra != nil { out["extra"] = status.Extra } if !ready { nextCommand := driveImportTaskResultCommand(ticket) fmt.Fprintf(runtime.IO().ErrOut, "Import task is still in progress. Continue with: %s\n", nextCommand) out["timed_out"] = true out["next_command"] = nextCommand } runtime.Out(out, nil) return nil }, }
DriveImport uploads a local file, creates an import task, and polls until the imported cloud document is ready or the local polling window expires.
var DriveMove = common.Shortcut{ Service: "drive", Command: "+move", Description: "Move a file or folder to another location in Drive", Risk: "write", Scopes: []string{"space:document:move"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "file-token", Desc: "file or folder token to move", Required: true}, {Name: "type", Desc: "file type (file, docx, bitable, doc, sheet, mindnote, folder, slides)", Required: true}, {Name: "folder-token", Desc: "target folder token (default: root folder)"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { return validateDriveMoveSpec(driveMoveSpec{ FileToken: runtime.Str("file-token"), FileType: strings.ToLower(runtime.Str("type")), FolderToken: runtime.Str("folder-token"), }) }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { spec := driveMoveSpec{ FileToken: runtime.Str("file-token"), FileType: strings.ToLower(runtime.Str("type")), FolderToken: runtime.Str("folder-token"), } dry := common.NewDryRunAPI(). Desc("Move file or folder in Drive") dry.POST("/open-apis/drive/v1/files/:file_token/move"). Desc("[1] Move file/folder"). Set("file_token", spec.FileToken). Body(spec.RequestBody()) if spec.FileType == "folder" { dry.GET("/open-apis/drive/v1/files/task_check"). Desc("[2] Poll async task status (for folder move)"). Params(driveTaskCheckParams("<task_id>")) } return dry }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { spec := driveMoveSpec{ FileToken: runtime.Str("file-token"), FileType: strings.ToLower(runtime.Str("type")), FolderToken: runtime.Str("folder-token"), } if spec.FolderToken == "" { fmt.Fprintf(runtime.IO().ErrOut, "No target folder specified, getting root folder...\n") rootToken, err := getRootFolderToken(ctx, runtime) if err != nil { return err } if rootToken == "" { return output.Errorf(output.ExitAPI, "api_error", "get root folder token failed, root folder is empty") } spec.FolderToken = rootToken } fmt.Fprintf(runtime.IO().ErrOut, "Moving %s %s to folder %s...\n", spec.FileType, common.MaskToken(spec.FileToken), common.MaskToken(spec.FolderToken)) data, err := runtime.CallAPI( "POST", fmt.Sprintf("/open-apis/drive/v1/files/%s/move", validate.EncodePathSegment(spec.FileToken)), nil, spec.RequestBody(), ) if err != nil { return err } if spec.FileType == "folder" { taskID := common.GetString(data, "task_id") if taskID == "" { return output.Errorf(output.ExitAPI, "api_error", "move folder returned no task_id") } fmt.Fprintf(runtime.IO().ErrOut, "Folder move is async, polling task %s...\n", taskID) status, ready, err := pollDriveTaskCheck(runtime, taskID) if err != nil { return err } out := map[string]interface{}{ "task_id": taskID, "status": status.StatusLabel(), "file_token": spec.FileToken, "folder_token": spec.FolderToken, "ready": ready, } if !ready { nextCommand := driveTaskCheckResultCommand(taskID) fmt.Fprintf(runtime.IO().ErrOut, "Folder move task is still in progress. Continue with: %s\n", nextCommand) out["timed_out"] = true out["next_command"] = nextCommand } runtime.Out(out, nil) } else { runtime.Out(map[string]interface{}{ "file_token": spec.FileToken, "folder_token": spec.FolderToken, "type": spec.FileType, }, nil) } return nil }, }
DriveMove moves a Drive file or folder and handles the async task polling required by folder moves.
var DriveTaskResult = common.Shortcut{ Service: "drive", Command: "+task_result", Description: "Poll async task result for import, export, move, or delete operations", Risk: "read", Scopes: []string{"drive:drive.metadata:readonly"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "ticket", Desc: "async task ticket (for import/export tasks)", Required: false}, {Name: "task-id", Desc: "async task ID (for move/delete folder tasks)", Required: false}, {Name: "scenario", Desc: "task scenario: import, export, or task_check", Required: true}, {Name: "file-token", Desc: "source document token used for export task status lookup", Required: false}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { scenario := strings.ToLower(runtime.Str("scenario")) validScenarios := map[string]bool{ "import": true, "export": true, "task_check": true, } if !validScenarios[scenario] { return output.ErrValidation("unsupported scenario: %s. Supported scenarios: import, export, task_check", scenario) } switch scenario { case "import", "export": if runtime.Str("ticket") == "" { return output.ErrValidation("--ticket is required for %s scenario", scenario) } if err := validate.ResourceName(runtime.Str("ticket"), "--ticket"); err != nil { return output.ErrValidation("%s", err) } case "task_check": if runtime.Str("task-id") == "" { return output.ErrValidation("--task-id is required for task_check scenario") } if err := validate.ResourceName(runtime.Str("task-id"), "--task-id"); err != nil { return output.ErrValidation("%s", err) } } if scenario == "export" && runtime.Str("file-token") == "" { return output.ErrValidation("--file-token is required for export scenario") } if scenario == "export" { if err := validate.ResourceName(runtime.Str("file-token"), "--file-token"); err != nil { return output.ErrValidation("%s", err) } } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { scenario := strings.ToLower(runtime.Str("scenario")) ticket := runtime.Str("ticket") taskID := runtime.Str("task-id") fileToken := runtime.Str("file-token") dry := common.NewDryRunAPI() dry.Desc(fmt.Sprintf("Poll async task result for %s scenario", scenario)) switch scenario { case "import": dry.GET("/open-apis/drive/v1/import_tasks/:ticket"). Desc("[1] Query import task result"). Set("ticket", ticket) case "export": dry.GET("/open-apis/drive/v1/export_tasks/:ticket"). Desc("[1] Query export task result"). Set("ticket", ticket). Params(map[string]interface{}{"token": fileToken}) case "task_check": dry.GET("/open-apis/drive/v1/files/task_check"). Desc("[1] Query move/delete folder task status"). Params(driveTaskCheckParams(taskID)) } return dry }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { scenario := strings.ToLower(runtime.Str("scenario")) ticket := runtime.Str("ticket") taskID := runtime.Str("task-id") fileToken := runtime.Str("file-token") fmt.Fprintf(runtime.IO().ErrOut, "Querying %s task result...\n", scenario) var result map[string]interface{} var err error switch scenario { case "import": result, err = queryImportTask(runtime, ticket) case "export": result, err = queryExportTask(runtime, ticket, fileToken) case "task_check": result, err = queryTaskCheck(runtime, taskID) } if err != nil { return err } runtime.Out(result, nil) return nil }, }
DriveTaskResult exposes a unified read path for the async task types produced by Drive import, export, and folder move flows.
var DriveUpload = common.Shortcut{ Service: "drive", Command: "+upload", Description: "Upload a local file to Drive", Risk: "write", Scopes: []string{"drive:file:upload"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "file", Desc: "local file path (files > 20MB use multipart upload automatically)", Required: true}, {Name: "folder-token", Desc: "target folder token (default: root)"}, {Name: "name", Desc: "uploaded file name (default: local file name)"}, }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { filePath := runtime.Str("file") folderToken := runtime.Str("folder-token") name := runtime.Str("name") fileName := name if fileName == "" { fileName = filepath.Base(filePath) } return common.NewDryRunAPI(). Desc("multipart/form-data upload (files > 20MB use chunked 3-step upload)"). POST("/open-apis/drive/v1/files/upload_all"). Body(map[string]interface{}{ "file_name": fileName, "parent_type": "explorer", "parent_node": folderToken, "file": "@" + filePath, }) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { filePath := runtime.Str("file") folderToken := runtime.Str("folder-token") name := runtime.Str("name") safeFilePath, err := validate.SafeInputPath(filePath) if err != nil { return output.ErrValidation("unsafe file path: %s", err) } filePath = safeFilePath fileName := name if fileName == "" { fileName = filepath.Base(filePath) } info, err := vfs.Stat(filePath) if err != nil { return output.ErrValidation("cannot read file: %s", err) } fileSize := info.Size() fmt.Fprintf(runtime.IO().ErrOut, "Uploading: %s (%s)\n", fileName, common.FormatSize(fileSize)) var fileToken string if fileSize > maxDriveUploadFileSize { fmt.Fprintf(runtime.IO().ErrOut, "File exceeds 20MB, using multipart upload\n") fileToken, err = uploadFileMultipart(ctx, runtime, filePath, fileName, folderToken, fileSize) } else { fileToken, err = uploadFileToDrive(ctx, runtime, filePath, fileName, folderToken, fileSize) } if err != nil { return err } runtime.Out(map[string]interface{}{ "file_token": fileToken, "file_name": fileName, "size": fileSize, }, nil) return nil }, }
Functions ¶
Types ¶
This section is empty.