drive

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 1, 2026 License: MIT Imports: 13 Imported by: 0

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

func Shortcuts

func Shortcuts() []common.Shortcut

Shortcuts returns all drive shortcuts.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL