Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
View Source
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 }, }
View Source
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)) apiResp, err := runtime.DoAPI(&larkcore.ApiReq{ HttpMethod: http.MethodGet, ApiPath: fmt.Sprintf("/open-apis/drive/v1/files/%s/download", validate.EncodePathSegment(fileToken)), }, larkcore.WithFileDownload()) if err != nil { return output.ErrNetwork("download failed: %s", err) } if apiResp.StatusCode >= 400 { return output.ErrNetwork("download failed: HTTP %d: %s", apiResp.StatusCode, string(apiResp.RawBody)) } os.MkdirAll(filepath.Dir(safePath), 0755) if err := validate.AtomicWrite(safePath, apiResp.RawBody, 0644); err != nil { return output.Errorf(output.ExitInternal, "api_error", "cannot create file: %s", err) } runtime.Out(map[string]interface{}{ "saved_path": safePath, "size_bytes": len(apiResp.RawBody), }, nil) return nil }, }
View Source
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 (max 20MB)", 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"). 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 := os.Stat(filePath) if err != nil { return output.ErrValidation("cannot read file: %s", err) } fileSize := info.Size() if fileSize > maxDriveUploadFileSize { return output.ErrValidation("file %.1fMB exceeds 20MB limit", float64(fileSize)/1024/1024) } fmt.Fprintf(runtime.IO().ErrOut, "Uploading: %s (%s)\n", fileName, common.FormatSize(fileSize)) 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.
Click to show internal directories.
Click to hide internal directories.