doc

package
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Mar 31, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DocMediaDownload = common.Shortcut{
	Service:     "docs",
	Command:     "+media-download",
	Description: "Download document media or whiteboard thumbnail (auto-detects extension)",
	Risk:        "read",
	Scopes:      []string{"docs:document.media:download"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "token", Desc: "resource token (file_token or whiteboard_id)", Required: true},
		{Name: "output", Desc: "local save path", Required: true},
		{Name: "type", Default: "media", Desc: "resource type: media (default) | whiteboard"},
		{Name: "overwrite", Type: "bool", Desc: "overwrite existing output file"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		token := runtime.Str("token")
		outputPath := runtime.Str("output")
		mediaType := runtime.Str("type")
		if mediaType == "whiteboard" {
			return common.NewDryRunAPI().
				GET("/open-apis/board/v1/whiteboards/:token/download_as_image").
				Desc("(when --type=whiteboard) Download whiteboard as image").
				Set("token", token).Set("output", outputPath)
		}
		return common.NewDryRunAPI().
			GET("/open-apis/drive/v1/medias/:token/download").
			Desc("(when --type=media) Download document media file").
			Set("token", token).Set("output", outputPath)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		token := runtime.Str("token")
		outputPath := runtime.Str("output")
		mediaType := runtime.Str("type")
		overwrite := runtime.Bool("overwrite")

		if err := validate.ResourceName(token, "--token"); err != nil {
			return output.ErrValidation("%s", err)
		}

		if _, err := validate.SafeOutputPath(outputPath); err != nil {
			return output.ErrValidation("unsafe output path: %s", err)
		}

		fmt.Fprintf(runtime.IO().ErrOut, "Downloading: %s %s\n", mediaType, common.MaskToken(token))

		encodedToken := validate.EncodePathSegment(token)
		var apiPath string
		if mediaType == "whiteboard" {
			apiPath = fmt.Sprintf("/open-apis/board/v1/whiteboards/%s/download_as_image", encodedToken)
		} else {
			apiPath = fmt.Sprintf("/open-apis/drive/v1/medias/%s/download", encodedToken)
		}

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod: http.MethodGet,
			ApiPath:    apiPath,
		}, larkcore.WithFileDownload())
		if err != nil {
			return output.ErrNetwork("download failed: %v", err)
		}
		if apiResp.StatusCode >= 400 {
			return output.ErrNetwork("download failed: HTTP %d: %s", apiResp.StatusCode, strings.TrimSpace(string(apiResp.RawBody)))
		}

		finalPath := outputPath
		currentExt := filepath.Ext(outputPath)
		if currentExt == "" {
			contentType := apiResp.Header.Get("Content-Type")
			mimeType := strings.Split(contentType, ";")[0]
			mimeType = strings.TrimSpace(mimeType)
			if ext, ok := mimeToExt[mimeType]; ok {
				finalPath = outputPath + ext
			} else if mediaType == "whiteboard" {
				finalPath = outputPath + ".png"
			}
		}

		safePath, err := validate.SafeOutputPath(finalPath)
		if err != nil {
			return output.ErrValidation("unsafe output path: %s", err)
		}
		if err := common.EnsureWritableFile(safePath, overwrite); err != nil {
			return err
		}

		os.MkdirAll(filepath.Dir(safePath), 0755)
		if err := validate.AtomicWrite(safePath, apiResp.RawBody, 0644); err != nil {
			return output.Errorf(output.ExitInternal, "io", "cannot create file: %v", err)
		}

		runtime.Out(map[string]interface{}{
			"saved_path":   safePath,
			"size_bytes":   len(apiResp.RawBody),
			"content_type": apiResp.Header.Get("Content-Type"),
		}, nil)
		return nil
	},
}
View Source
var DocMediaInsert = common.Shortcut{
	Service:     "docs",
	Command:     "+media-insert",
	Description: "Insert a local image or file at the end of a Lark document (4-step orchestration + auto-rollback)",
	Risk:        "write",
	Scopes:      []string{"docs:document.media:upload", "docx:document:write_only", "docx:document:readonly"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "file", Desc: "local file path (max 20MB)", Required: true},
		{Name: "doc", Desc: "document URL or document_id", Required: true},
		{Name: "type", Default: "image", Desc: "type: image | file"},
		{Name: "align", Desc: "alignment: left | center | right"},
		{Name: "caption", Desc: "image caption text"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		docRef, err := parseDocumentRef(runtime.Str("doc"))
		if err != nil {
			return err
		}
		if docRef.Kind == "doc" {
			return output.ErrValidation("docs +media-insert only supports docx documents; use a docx token/URL or a wiki URL that resolves to docx")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		docRef, err := parseDocumentRef(runtime.Str("doc"))
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}

		documentID := docRef.Token
		stepBase := 1
		filePath := runtime.Str("file")
		mediaType := runtime.Str("type")
		caption := runtime.Str("caption")

		parentType := parentTypeForMediaType(mediaType)
		createBlockData := buildCreateBlockData(mediaType, 0)
		createBlockData["index"] = "<children_len>"
		batchUpdateData := buildBatchUpdateData("<new_block_id>", mediaType, "<file_token>", runtime.Str("align"), caption)

		d := common.NewDryRunAPI()
		if docRef.Kind == "wiki" {
			documentID = "<resolved_docx_token>"
			stepBase = 2
			d.Desc("5-step orchestration: resolve wiki → query root → create block → upload file → bind to block (auto-rollback on failure)").
				GET("/open-apis/wiki/v2/spaces/get_node").
				Desc("[1] Resolve wiki node to docx document").
				Params(map[string]interface{}{"token": docRef.Token})
		} else {
			d.Desc("4-step orchestration: query root → create block → upload file → bind to block (auto-rollback on failure)")
		}

		d.
			GET("/open-apis/docx/v1/documents/:document_id/blocks/:document_id").
			Desc(fmt.Sprintf("[%d] Get document root block", stepBase)).
			POST("/open-apis/docx/v1/documents/:document_id/blocks/:document_id/children").
			Desc(fmt.Sprintf("[%d] Create empty block at document end", stepBase+1)).
			Body(createBlockData).
			POST("/open-apis/drive/v1/medias/upload_all").
			Desc(fmt.Sprintf("[%d] Upload local file (multipart/form-data)", stepBase+2)).
			Body(map[string]interface{}{
				"file_name":   filepath.Base(filePath),
				"parent_type": parentType,
				"parent_node": "<new_block_id>",
				"file":        "@" + filePath,
			}).
			PATCH("/open-apis/docx/v1/documents/:document_id/blocks/batch_update").
			Desc(fmt.Sprintf("[%d] Bind uploaded file token to the new block", stepBase+3)).
			Body(batchUpdateData)

		return d.Set("document_id", documentID)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		filePath := runtime.Str("file")
		docInput := runtime.Str("doc")
		mediaType := runtime.Str("type")
		alignStr := runtime.Str("align")
		caption := runtime.Str("caption")

		safeFilePath, pathErr := validate.SafeInputPath(filePath)
		if pathErr != nil {
			return output.ErrValidation("unsafe file path: %s", pathErr)
		}
		filePath = safeFilePath

		documentID, err := resolveDocxDocumentID(runtime, docInput)
		if err != nil {
			return err
		}

		stat, err := os.Stat(filePath)
		if err != nil {
			return output.ErrValidation("file not found: %s", filePath)
		}
		if stat.Size() > maxFileSize {
			return output.ErrValidation("file %.1fMB exceeds 20MB limit", float64(stat.Size())/1024/1024)
		}

		fileName := filepath.Base(filePath)
		fmt.Fprintf(runtime.IO().ErrOut, "Inserting: %s -> document %s\n", fileName, common.MaskToken(documentID))

		rootData, err := runtime.CallAPI("GET",
			fmt.Sprintf("/open-apis/docx/v1/documents/%s/blocks/%s", validate.EncodePathSegment(documentID), validate.EncodePathSegment(documentID)),
			nil, nil)
		if err != nil {
			return err
		}

		parentBlockID, insertIndex, err := extractAppendTarget(rootData, documentID)
		if err != nil {
			return err
		}
		fmt.Fprintf(runtime.IO().ErrOut, "Root block ready: %s (%d children)\n", parentBlockID, insertIndex)

		fmt.Fprintf(runtime.IO().ErrOut, "Creating block at index %d\n", insertIndex)

		createData, err := runtime.CallAPI("POST",
			fmt.Sprintf("/open-apis/docx/v1/documents/%s/blocks/%s/children", validate.EncodePathSegment(documentID), validate.EncodePathSegment(parentBlockID)),
			nil, buildCreateBlockData(mediaType, insertIndex))
		if err != nil {
			return err
		}

		blockId, uploadParentNode, replaceBlockID := extractCreatedBlockTargets(createData, mediaType)

		if blockId == "" {
			return output.Errorf(output.ExitAPI, "api_error", "failed to create block: no block_id returned")
		}

		fmt.Fprintf(runtime.IO().ErrOut, "Block created: %s\n", blockId)
		if uploadParentNode != blockId || replaceBlockID != blockId {
			fmt.Fprintf(runtime.IO().ErrOut, "Resolved file block targets: upload=%s replace=%s\n", uploadParentNode, replaceBlockID)
		}

		rollback := func() error {
			fmt.Fprintf(runtime.IO().ErrOut, "Rolling back: deleting block %s\n", blockId)
			_, err := runtime.CallAPI("DELETE",
				fmt.Sprintf("/open-apis/docx/v1/documents/%s/blocks/%s/children/batch_delete", validate.EncodePathSegment(documentID), validate.EncodePathSegment(parentBlockID)),
				nil, buildDeleteBlockData(insertIndex))
			return err
		}
		withRollbackWarning := func(opErr error) error {
			rollbackErr := rollback()
			if rollbackErr == nil {
				return opErr
			}
			warning := fmt.Sprintf("rollback failed for block %s: %v", blockId, rollbackErr)
			fmt.Fprintf(runtime.IO().ErrOut, "warning: %s\n", warning)
			return opErr
		}

		fileToken, err := uploadMediaFile(ctx, runtime, filePath, fileName, mediaType, uploadParentNode, documentID)
		if err != nil {
			return withRollbackWarning(err)
		}

		fmt.Fprintf(runtime.IO().ErrOut, "File uploaded: %s\n", fileToken)

		fmt.Fprintf(runtime.IO().ErrOut, "Binding uploaded media to block %s\n", replaceBlockID)

		if _, err := runtime.CallAPI("PATCH",
			fmt.Sprintf("/open-apis/docx/v1/documents/%s/blocks/batch_update", validate.EncodePathSegment(documentID)),
			nil, buildBatchUpdateData(replaceBlockID, mediaType, fileToken, alignStr, caption)); err != nil {
			return withRollbackWarning(err)
		}

		runtime.Out(map[string]interface{}{
			"document_id": documentID,
			"block_id":    blockId,
			"file_token":  fileToken,
			"type":        mediaType,
		}, nil)
		return nil
	},
}
View Source
var DocsCreate = common.Shortcut{
	Service:     "docs",
	Command:     "+create",
	Description: "Create a Lark document",
	Risk:        "write",
	AuthTypes:   []string{"user", "bot"},
	Scopes:      []string{"docx:document:create"},
	Flags: []common.Flag{
		{Name: "title", Desc: "document title"},
		{Name: "markdown", Desc: "Markdown content (Lark-flavored)", Required: true},
		{Name: "folder-token", Desc: "parent folder token"},
		{Name: "wiki-node", Desc: "wiki node token"},
		{Name: "wiki-space", Desc: "wiki space ID (use my_library for personal library)"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		count := 0
		if runtime.Str("folder-token") != "" {
			count++
		}
		if runtime.Str("wiki-node") != "" {
			count++
		}
		if runtime.Str("wiki-space") != "" {
			count++
		}
		if count > 1 {
			return common.FlagErrorf("--folder-token, --wiki-node, and --wiki-space are mutually exclusive")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		args := map[string]interface{}{
			"markdown": runtime.Str("markdown"),
		}
		if v := runtime.Str("title"); v != "" {
			args["title"] = v
		}
		if v := runtime.Str("folder-token"); v != "" {
			args["folder_token"] = v
		}
		if v := runtime.Str("wiki-node"); v != "" {
			args["wiki_node"] = v
		}
		if v := runtime.Str("wiki-space"); v != "" {
			args["wiki_space"] = v
		}
		return common.NewDryRunAPI().
			POST(common.MCPEndpoint(runtime.Config.Brand)).
			Desc("MCP tool: create-doc").
			Body(map[string]interface{}{"method": "tools/call", "params": map[string]interface{}{"name": "create-doc", "arguments": args}}).
			Set("mcp_tool", "create-doc").Set("args", args)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		args := map[string]interface{}{
			"markdown": runtime.Str("markdown"),
		}
		if v := runtime.Str("title"); v != "" {
			args["title"] = v
		}
		if v := runtime.Str("folder-token"); v != "" {
			args["folder_token"] = v
		}
		if v := runtime.Str("wiki-node"); v != "" {
			args["wiki_node"] = v
		}
		if v := runtime.Str("wiki-space"); v != "" {
			args["wiki_space"] = v
		}

		result, err := common.CallMCPTool(runtime, "create-doc", args)
		if err != nil {
			return err
		}

		runtime.Out(result, nil)
		return nil
	},
}
View Source
var DocsFetch = common.Shortcut{
	Service:     "docs",
	Command:     "+fetch",
	Description: "Fetch Lark document content",
	Risk:        "read",
	Scopes:      []string{"docx:document:readonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "doc", Desc: "document URL or token", Required: true},
		{Name: "offset", Desc: "pagination offset"},
		{Name: "limit", Desc: "pagination limit"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		args := map[string]interface{}{
			"doc_id": runtime.Str("doc"),
		}
		if v := runtime.Str("offset"); v != "" {
			n, _ := strconv.Atoi(v)
			args["offset"] = n
		}
		if v := runtime.Str("limit"); v != "" {
			n, _ := strconv.Atoi(v)
			args["limit"] = n
		}
		return common.NewDryRunAPI().
			POST(common.MCPEndpoint(runtime.Config.Brand)).
			Desc("MCP tool: fetch-doc").
			Body(map[string]interface{}{"method": "tools/call", "params": map[string]interface{}{"name": "fetch-doc", "arguments": args}}).
			Set("mcp_tool", "fetch-doc").Set("args", args)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		args := map[string]interface{}{
			"doc_id": runtime.Str("doc"),
		}
		if v := runtime.Str("offset"); v != "" {
			n, _ := strconv.Atoi(v)
			args["offset"] = n
		}
		if v := runtime.Str("limit"); v != "" {
			n, _ := strconv.Atoi(v)
			args["limit"] = n
		}

		result, err := common.CallMCPTool(runtime, "fetch-doc", args)
		if err != nil {
			return err
		}

		runtime.OutFormat(result, nil, func(w io.Writer) {
			if title, ok := result["title"].(string); ok && title != "" {
				fmt.Fprintf(w, "# %s\n\n", title)
			}
			if md, ok := result["markdown"].(string); ok {
				fmt.Fprintln(w, md)
			}
			if hasMore, ok := result["has_more"].(bool); ok && hasMore {
				fmt.Fprintln(w, "\n--- more content available, use --offset and --limit to paginate ---")
			}
		})
		return nil
	},
}
View Source
var DocsSearch = common.Shortcut{
	Service:     "docs",
	Command:     "+search",
	Description: "Search Lark docs, Wiki, and spreadsheet files (Search v2: doc_wiki/search)",
	Risk:        "read",
	Scopes:      []string{"search:docs:read"},
	AuthTypes:   []string{"user"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "query", Desc: "search keyword"},
		{Name: "filter", Desc: "filter conditions (JSON object)"},
		{Name: "page-token", Desc: "page token"},
		{Name: "page-size", Default: "15", Desc: "page size (default 15, max 20)"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		requestData, err := buildDocsSearchRequest(
			runtime.Str("query"),
			runtime.Str("filter"),
			runtime.Str("page-token"),
			runtime.Str("page-size"),
		)
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}

		return common.NewDryRunAPI().
			POST("/open-apis/search/v2/doc_wiki/search").
			Body(requestData)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		requestData, err := buildDocsSearchRequest(
			runtime.Str("query"),
			runtime.Str("filter"),
			runtime.Str("page-token"),
			runtime.Str("page-size"),
		)
		if err != nil {
			return err
		}

		data, err := runtime.CallAPI("POST", "/open-apis/search/v2/doc_wiki/search", nil, requestData)
		if err != nil {
			return err
		}
		items, _ := data["res_units"].([]interface{})

		normalizedItems := addIsoTimeFields(items)

		resultData := map[string]interface{}{
			"total":      data["total"],
			"has_more":   data["has_more"],
			"page_token": data["page_token"],
			"results":    normalizedItems,
		}

		runtime.OutFormat(resultData, &output.Meta{Count: len(normalizedItems)}, func(w io.Writer) {
			if len(normalizedItems) == 0 {
				fmt.Fprintln(w, "No matching results found.")
				return
			}

			htmlTagRe := regexp.MustCompile(`</?h>`)
			var rows []map[string]interface{}
			for _, item := range normalizedItems {
				u, _ := item.(map[string]interface{})
				if u == nil {
					continue
				}

				rawTitle := fmt.Sprintf("%v", u["title_highlighted"])
				title := htmlTagRe.ReplaceAllString(rawTitle, "")
				title = common.TruncateStr(title, 50)

				resultMeta, _ := u["result_meta"].(map[string]interface{})
				docTypes := ""
				if resultMeta != nil {
					docTypes = fmt.Sprintf("%v", resultMeta["doc_types"])
				}
				entityType := fmt.Sprintf("%v", u["entity_type"])
				typeStr := docTypes
				if typeStr == "" || typeStr == "<nil>" {
					typeStr = entityType
				}

				url := ""
				editTime := ""
				if resultMeta != nil {
					url = fmt.Sprintf("%v", resultMeta["url"])
					editTime = fmt.Sprintf("%v", resultMeta["update_time_iso"])
				}
				if len(url) > 80 {
					url = url[:80]
				}

				rows = append(rows, map[string]interface{}{
					"type":      typeStr,
					"title":     title,
					"edit_time": editTime,
					"url":       url,
				})
			}

			output.PrintTable(w, rows)
			moreHint := ""
			hasMore, _ := data["has_more"].(bool)
			if hasMore {
				moreHint = " (more available, use --format json to get page_token, then --page-token to paginate)"
			}
			fmt.Fprintf(w, "\n%d result(s)%s\n", len(rows), moreHint)
		})
		return nil
	},
}
View Source
var DocsUpdate = common.Shortcut{
	Service:     "docs",
	Command:     "+update",
	Description: "Update a Lark document",
	Risk:        "write",
	Scopes:      []string{"docx:document:write_only", "docx:document:readonly"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "doc", Desc: "document URL or token", Required: true},
		{Name: "mode", Desc: "update mode: append | overwrite | replace_range | replace_all | insert_before | insert_after | delete_range", Required: true},
		{Name: "markdown", Desc: "new content (Lark-flavored Markdown; create blank whiteboards with <whiteboard type=\"blank\"></whiteboard>, repeat to create multiple boards)"},
		{Name: "selection-with-ellipsis", Desc: "content locator (e.g. 'start...end')"},
		{Name: "selection-by-title", Desc: "title locator (e.g. '## Section')"},
		{Name: "new-title", Desc: "also update document title"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		mode := runtime.Str("mode")
		if !validModes[mode] {
			return common.FlagErrorf("invalid --mode %q, valid: append | overwrite | replace_range | replace_all | insert_before | insert_after | delete_range", mode)
		}

		if mode != "delete_range" && runtime.Str("markdown") == "" {
			return common.FlagErrorf("--%s mode requires --markdown", mode)
		}

		selEllipsis := runtime.Str("selection-with-ellipsis")
		selTitle := runtime.Str("selection-by-title")
		if selEllipsis != "" && selTitle != "" {
			return common.FlagErrorf("--selection-with-ellipsis and --selection-by-title are mutually exclusive")
		}

		if needsSelection[mode] && selEllipsis == "" && selTitle == "" {
			return common.FlagErrorf("--%s mode requires --selection-with-ellipsis or --selection-by-title", mode)
		}

		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		args := map[string]interface{}{
			"doc_id": runtime.Str("doc"),
			"mode":   runtime.Str("mode"),
		}
		if v := runtime.Str("markdown"); v != "" {
			args["markdown"] = v
		}
		if v := runtime.Str("selection-with-ellipsis"); v != "" {
			args["selection_with_ellipsis"] = v
		}
		if v := runtime.Str("selection-by-title"); v != "" {
			args["selection_by_title"] = v
		}
		if v := runtime.Str("new-title"); v != "" {
			args["new_title"] = v
		}
		return common.NewDryRunAPI().
			POST(common.MCPEndpoint(runtime.Config.Brand)).
			Desc("MCP tool: update-doc").
			Body(map[string]interface{}{"method": "tools/call", "params": map[string]interface{}{"name": "update-doc", "arguments": args}}).
			Set("mcp_tool", "update-doc").Set("args", args)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		args := map[string]interface{}{
			"doc_id": runtime.Str("doc"),
			"mode":   runtime.Str("mode"),
		}
		if v := runtime.Str("markdown"); v != "" {
			args["markdown"] = v
		}
		if v := runtime.Str("selection-with-ellipsis"); v != "" {
			args["selection_with_ellipsis"] = v
		}
		if v := runtime.Str("selection-by-title"); v != "" {
			args["selection_by_title"] = v
		}
		if v := runtime.Str("new-title"); v != "" {
			args["new_title"] = v
		}

		result, err := common.CallMCPTool(runtime, "update-doc", args)
		if err != nil {
			return err
		}

		normalizeDocsUpdateResult(result, runtime.Str("markdown"))
		runtime.Out(result, nil)
		return nil
	},
}
View Source
var MediaUpload = common.Shortcut{
	Service:     "docs",
	Command:     "+media-upload",
	Description: "Upload media file (image/attachment) to a document block",
	Risk:        "write",
	Scopes:      []string{"docs:document.media:upload"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "file", Desc: "local file path (max 20MB)", Required: true},
		{Name: "parent-type", Desc: "parent type: docx_image | docx_file", Required: true},
		{Name: "parent-node", Desc: "parent node ID (block_id)", Required: true},
		{Name: "doc-id", Desc: "document ID (for drive_route_token)"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		filePath := runtime.Str("file")
		parentType := runtime.Str("parent-type")
		parentNode := runtime.Str("parent-node")
		docId := runtime.Str("doc-id")
		body := map[string]interface{}{
			"file_name":   filepath.Base(filePath),
			"parent_type": parentType,
			"parent_node": parentNode,
			"file":        "@" + filePath,
		}
		if docId != "" {
			body["extra"] = fmt.Sprintf(`{"drive_route_token":"%s"}`, docId)
		}
		return common.NewDryRunAPI().
			Desc("multipart/form-data upload").
			POST("/open-apis/drive/v1/medias/upload_all").
			Body(body)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		filePath := runtime.Str("file")
		parentType := runtime.Str("parent-type")
		parentNode := runtime.Str("parent-node")
		docId := runtime.Str("doc-id")

		safeFilePath, pathErr := validate.SafeInputPath(filePath)
		if pathErr != nil {
			return output.ErrValidation("unsafe file path: %s", pathErr)
		}
		filePath = safeFilePath

		stat, err := os.Stat(filePath)
		if err != nil {
			return output.ErrValidation("file not found: %s", filePath)
		}
		if stat.Size() > maxFileSize {
			return output.ErrValidation("file %.1fMB exceeds 20MB limit", float64(stat.Size())/1024/1024)
		}

		fileName := filepath.Base(filePath)
		fmt.Fprintf(runtime.IO().ErrOut, "Uploading: %s (%d bytes)\n", fileName, stat.Size())

		f, err := os.Open(filePath)
		if err != nil {
			return output.ErrValidation("cannot open file: %v", err)
		}
		defer f.Close()

		fd := larkcore.NewFormdata()
		fd.AddField("file_name", fileName)
		fd.AddField("parent_type", parentType)
		fd.AddField("parent_node", parentNode)
		fd.AddField("size", fmt.Sprintf("%d", stat.Size()))
		if docId != "" {
			extra, err := buildDriveRouteExtra(docId)
			if err != nil {
				return err
			}
			fd.AddField("extra", extra)
		}
		fd.AddFile("file", f)

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod: http.MethodPost,
			ApiPath:    "/open-apis/drive/v1/medias/upload_all",
			Body:       fd,
		}, larkcore.WithFileUpload())
		if err != nil {
			var exitErr *output.ExitError
			if errors.As(err, &exitErr) {
				return err
			}
			return output.ErrNetwork("upload failed: %v", err)
		}

		var result map[string]interface{}
		if err := json.Unmarshal(apiResp.RawBody, &result); err != nil {
			return output.Errorf(output.ExitAPI, "api_error", "upload failed: invalid response JSON: %v", err)
		}

		code, _ := util.ToFloat64(result["code"])
		if code != 0 {
			msg, _ := result["msg"].(string)
			return output.ErrAPI(int(code), fmt.Sprintf("upload failed: [%d] %s", int(code), msg), result["error"])
		}

		data, _ := result["data"].(map[string]interface{})
		fileToken, _ := data["file_token"].(string)
		if fileToken == "" {
			return output.Errorf(output.ExitAPI, "api_error", "upload failed: no file_token returned")
		}

		runtime.Out(map[string]interface{}{
			"file_token": fileToken,
			"file_name":  fileName,
			"size":       stat.Size(),
		}, nil)
		return nil
	},
}

Functions

func Shortcuts

func Shortcuts() []common.Shortcut

Shortcuts returns all docs shortcuts.

Types

This section is empty.

Jump to

Keyboard shortcuts

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