Documentation
¶
Index ¶
Constants ¶
const PreviewType_SOURCE_FILE = "16"
Variables ¶
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 := runtime.ResolveSavePath(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) } resp, err := runtime.DoAPIStream(ctx, &larkcore.ApiReq{ HttpMethod: http.MethodGet, ApiPath: apiPath, }) if err != nil { return output.ErrNetwork("download failed: %v", err) } defer resp.Body.Close() fallbackExt := "" if mediaType == "whiteboard" { fallbackExt = ".png" } finalPath, _ := autoAppendDocMediaExtension(outputPath, resp.Header, fallbackExt) if finalPath != outputPath { if _, err := runtime.ResolveSavePath(finalPath); err != nil { return output.ErrValidation("unsafe output path: %s", err) } } if !overwrite { if _, statErr := runtime.FileIO().Stat(finalPath); statErr == nil { return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", finalPath) } } result, err := runtime.FileIO().Save(finalPath, fileio.SaveOptions{ ContentType: resp.Header.Get("Content-Type"), ContentLength: resp.ContentLength, }, resp.Body) if err != nil { return common.WrapSaveErrorByCategory(err, "io") } savedPath, _ := runtime.ResolveSavePath(finalPath) if savedPath == "" { savedPath = finalPath } runtime.Out(map[string]interface{}{ "saved_path": savedPath, "size_bytes": result.Size(), "content_type": resp.Header.Get("Content-Type"), }, nil) return nil }, }
var DocMediaInsert = common.Shortcut{ Service: "docs", Command: "+media-insert", Description: "Insert a local image or file into a Lark document (4-step orchestration + auto-rollback); appends to end by default, or inserts relative to a text selection with --selection-with-ellipsis", 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 (files > 20MB use multipart upload automatically)"}, {Name: "from-clipboard", Type: "bool", Desc: "read image from system clipboard instead of a local file (macOS/Windows built-in; Linux requires xclip, xsel or wl-paste)"}, {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"}, {Name: "selection-with-ellipsis", Desc: "plain text (or 'start...end' to disambiguate) matching the target block's content. Media is inserted at the top-level ancestor of the matched block — i.e., when the selection is inside a callout, table cell, or nested list, media lands outside that container, not inside it. Pass 'start...end' (a unique prefix and suffix separated by '...') when the plain text appears in more than one block"}, {Name: "before", Type: "bool", Desc: "insert before the matched block instead of after (requires --selection-with-ellipsis)"}, {Name: "file-view", Desc: "file block rendering: card (default) | preview | inline; only applies when --type=file. preview renders audio/video as an inline player"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { filePath := runtime.Str("file") fromClipboard := runtime.Bool("from-clipboard") if filePath == "" && !fromClipboard { return common.FlagErrorf("one of --file or --from-clipboard is required") } if filePath != "" && fromClipboard { return common.FlagErrorf("--file and --from-clipboard are mutually exclusive") } 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") } rawSelection := runtime.Str("selection-with-ellipsis") trimmedSelection := strings.TrimSpace(rawSelection) if rawSelection != "" && trimmedSelection == "" { return output.ErrValidation("--selection-with-ellipsis must not be blank or whitespace-only") } if runtime.Bool("before") && trimmedSelection == "" { return output.ErrValidation("--before requires --selection-with-ellipsis") } if view := runtime.Str("file-view"); view != "" { if _, ok := fileViewMap[view]; !ok { return output.ErrValidation("invalid --file-view value %q, expected one of: card | preview | inline", view) } if runtime.Str("type") != "file" { return output.ErrValidation("--file-view only applies when --type=file") } } 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") if runtime.Bool("from-clipboard") { filePath = "<clipboard image>" } mediaType := runtime.Str("type") caption := runtime.Str("caption") selection := strings.TrimSpace(runtime.Str("selection-with-ellipsis")) hasSelection := selection != "" fileViewType := fileViewMap[runtime.Str("file-view")] parentType := parentTypeForMediaType(mediaType) createBlockData := buildCreateBlockData(mediaType, 0, fileViewType) if hasSelection { createBlockData["index"] = "<locate_index>" } else { createBlockData["index"] = "<children_len>" } batchUpdateData := buildBatchUpdateData("<new_block_id>", mediaType, "<file_token>", runtime.Str("align"), caption) d := common.NewDryRunAPI() totalSteps := 4 if docRef.Kind == "wiki" { totalSteps++ } if hasSelection { totalSteps++ } positionLabel := map[bool]string{true: "before", false: "after"}[runtime.Bool("before")] if docRef.Kind == "wiki" { documentID = "<resolved_docx_token>" stepBase = 2 d.Desc(fmt.Sprintf("%d-step orchestration: resolve wiki → query root →%s create block → upload file → bind to block (auto-rollback on failure)", totalSteps, map[bool]string{true: " locate-doc →", false: ""}[hasSelection])). 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(fmt.Sprintf("%d-step orchestration: query root →%s create block → upload file → bind to block (auto-rollback on failure)", totalSteps, map[bool]string{true: " locate-doc →", false: ""}[hasSelection])) } d. GET("/open-apis/docx/v1/documents/:document_id/blocks/:document_id"). Desc(fmt.Sprintf("[%d] Get document root block", stepBase)) if hasSelection { mcpEndpoint := common.MCPEndpoint(runtime.Config.Brand) mcpArgs := map[string]interface{}{ "doc_id": documentID, "selection_with_ellipsis": selection, "limit": 1, } d.POST(mcpEndpoint). Desc(fmt.Sprintf("[%d] MCP locate-doc: find block matching selection (%s)", stepBase+1, positionLabel)). Body(map[string]interface{}{ "method": "tools/call", "params": map[string]interface{}{ "name": "locate-doc", "arguments": mcpArgs, }, }). Set("mcp_tool", "locate-doc"). Set("args", mcpArgs) stepBase++ } d. POST("/open-apis/docx/v1/documents/:document_id/blocks/:document_id/children"). Desc(fmt.Sprintf("[%d] Create empty block at target position", stepBase+1)). Body(createBlockData) appendDocMediaInsertUploadDryRun(d, runtime.FileIO(), filePath, parentType, stepBase+2) d.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) d.Set("document_id", documentID) if runtime.Bool("from-clipboard") { d.Set("upload_size_note", "clipboard size unknown; single-part vs multipart decision deferred to runtime") } return d }, 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") fileViewType := fileViewMap[runtime.Str("file-view")] // Clipboard path: read image bytes into memory, bypassing FileIO path validation. var clipboardContent []byte if runtime.Bool("from-clipboard") { fmt.Fprintf(runtime.IO().ErrOut, "Reading image from clipboard...\n") var err error clipboardContent, err = readClipboardImage() if err != nil { return err } } documentID, err := resolveDocxDocumentID(runtime, docInput) if err != nil { return err } // Determine file size and name. var fileSize int64 var fileName string if clipboardContent != nil { fileSize = int64(len(clipboardContent)) fileName = "clipboard.png" } else { stat, err := runtime.FileIO().Stat(filePath) if err != nil { return common.WrapInputStatError(err, "file not found") } if !stat.Mode().IsRegular() { return output.ErrValidation("file must be a regular file: %s", filePath) } fileSize = stat.Size() fileName = filepath.Base(filePath) } fmt.Fprintf(runtime.IO().ErrOut, "Inserting: %s -> document %s\n", fileName, common.MaskToken(documentID)) if fileSize > common.MaxDriveMediaUploadSinglePartSize { fmt.Fprintf(runtime.IO().ErrOut, "File exceeds 20MB, using multipart upload\n") } 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, rootChildren, err := extractAppendTarget(rootData, documentID) if err != nil { return err } fmt.Fprintf(runtime.IO().ErrOut, "Root block ready: %s (%d children)\n", parentBlockID, insertIndex) selection := strings.TrimSpace(runtime.Str("selection-with-ellipsis")) if selection != "" { before := runtime.Bool("before") fmt.Fprintf(runtime.IO().ErrOut, "Locating block matching selection (%s)\n", redactSelection(selection)) idx, err := locateInsertIndex(runtime, documentID, selection, rootChildren, before) if err != nil { return err } insertIndex = idx posLabel := "after" if before { posLabel = "before" } fmt.Fprintf(runtime.IO().ErrOut, "locate-doc matched: inserting %s at index %d\n", posLabel, 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, fileViewType)) 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 } uploadCfg := UploadDocMediaFileConfig{ FilePath: filePath, FileName: fileName, FileSize: fileSize, ParentType: parentTypeForMediaType(mediaType), ParentNode: uploadParentNode, DocID: documentID, } if clipboardContent != nil { uploadCfg.Reader = bytes.NewReader(clipboardContent) } fileToken, err := uploadDocMediaFile(runtime, uploadCfg) 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 }, }
var DocMediaPreview = common.Shortcut{ Service: "docs", Command: "+media-preview", Description: "Preview document media file (auto-detects extension)", Risk: "read", Scopes: []string{"docs:document.media:download"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "token", Desc: "media file token", Required: true}, {Name: "output", Desc: "local save path", Required: true}, {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") return common.NewDryRunAPI(). GET("/open-apis/drive/v1/medias/:token/preview_download"). Desc("Preview document media file"). Params(map[string]interface{}{"preview_type": PreviewType_SOURCE_FILE}). Set("token", token).Set("output", outputPath) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { token := runtime.Str("token") outputPath := runtime.Str("output") overwrite := runtime.Bool("overwrite") if err := validate.ResourceName(token, "--token"); err != nil { return output.ErrValidation("%s", err) } if _, err := runtime.ResolveSavePath(outputPath); err != nil { return output.ErrValidation("unsafe output path: %s", err) } fmt.Fprintf(runtime.IO().ErrOut, "Previewing: media %s\n", common.MaskToken(token)) encodedToken := validate.EncodePathSegment(token) apiPath := fmt.Sprintf("/open-apis/drive/v1/medias/%s/preview_download", encodedToken) resp, err := runtime.DoAPIStream(ctx, &larkcore.ApiReq{ HttpMethod: http.MethodGet, ApiPath: apiPath, QueryParams: larkcore.QueryParams{ "preview_type": []string{PreviewType_SOURCE_FILE}, }, }) if err != nil { return output.ErrNetwork("preview failed: %v", err) } defer resp.Body.Close() finalPath, _ := autoAppendDocMediaExtension(outputPath, resp.Header, "") if finalPath != outputPath { if _, err := runtime.ResolveSavePath(finalPath); err != nil { return output.ErrValidation("unsafe output path: %s", err) } } if !overwrite { if _, statErr := runtime.FileIO().Stat(finalPath); statErr == nil { return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", finalPath) } } result, err := runtime.FileIO().Save(finalPath, fileio.SaveOptions{ ContentType: resp.Header.Get("Content-Type"), ContentLength: resp.ContentLength, }, resp.Body) if err != nil { return common.WrapSaveErrorByCategory(err, "io") } savedPath, _ := runtime.ResolveSavePath(finalPath) runtime.Out(map[string]interface{}{ "saved_path": savedPath, "size_bytes": result.Size(), "content_type": resp.Header.Get("Content-Type"), }, nil) return nil }, }
var DocMediaUpload = 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 (files > 20MB use multipart upload automatically)", Required: true}, {Name: "parent-type", Desc: "parent type: docx_image | docx_file | whiteboard", Required: true}, {Name: "parent-node", Desc: "parent node ID (block_id for docx, board_token for whiteboard)", 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, } if docId != "" { body["extra"] = fmt.Sprintf(`{"drive_route_token":"%s"}`, docId) } dry := common.NewDryRunAPI() if docMediaShouldUseMultipart(runtime.FileIO(), filePath) { prepareBody := map[string]interface{}{ "file_name": filepath.Base(filePath), "parent_type": parentType, "parent_node": parentNode, "size": "<file_size>", } if extra, ok := body["extra"]; ok { prepareBody["extra"] = extra } dry.Desc("chunked media upload (files > 20MB)"). POST("/open-apis/drive/v1/medias/upload_prepare"). Body(prepareBody). POST("/open-apis/drive/v1/medias/upload_part"). Body(map[string]interface{}{ "upload_id": "<upload_id>", "seq": "<chunk_index>", "size": "<chunk_size>", "file": "<chunk_binary>", }). POST("/open-apis/drive/v1/medias/upload_finish"). Body(map[string]interface{}{ "upload_id": "<upload_id>", "block_num": "<block_num>", }) return dry } body["file"] = "@" + filePath body["size"] = "<file_size>" return dry.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") stat, err := runtime.FileIO().Stat(filePath) if err != nil { return common.WrapInputStatError(err, "file not found") } if !stat.Mode().IsRegular() { return output.ErrValidation("file must be a regular file: %s", filePath) } fileName := filepath.Base(filePath) fmt.Fprintf(runtime.IO().ErrOut, "Uploading: %s (%d bytes)\n", fileName, stat.Size()) if stat.Size() > common.MaxDriveMediaUploadSinglePartSize { fmt.Fprintf(runtime.IO().ErrOut, "File exceeds 20MB, using multipart upload\n") } fileToken, err := uploadDocMediaFile(runtime, UploadDocMediaFileConfig{ FilePath: filePath, FileName: fileName, FileSize: stat.Size(), ParentType: parentType, ParentNode: parentNode, DocID: docId, }) if err != nil { return err } runtime.Out(map[string]interface{}{ "file_token": fileToken, "file_name": fileName, "size": stat.Size(), }, nil) return nil }, }
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: concatFlags( []common.Flag{ {Name: "api-version", Desc: "API version", Default: "v1", Enum: []string{"v1", "v2"}}, }, v1CreateFlags(), v2CreateFlags(), ), Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if useV2Create(runtime) { return validateCreateV2(ctx, runtime) } return validateCreateV1(ctx, runtime) }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { if useV2Create(runtime) { return dryRunCreateV2(ctx, runtime) } return dryRunCreateV1(ctx, runtime) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { if useV2Create(runtime) { return executeCreateV2(ctx, runtime) } return executeCreateV1(ctx, runtime) }, PostMount: func(cmd *cobra.Command) { installVersionedHelp(cmd, "v1", docsCreateFlagVersions) }, }
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: concatFlags( []common.Flag{ {Name: "api-version", Desc: "API version", Default: "v1", Enum: []string{"v1", "v2"}}, {Name: "doc", Desc: "document URL or token", Required: true}, }, v1FetchFlags(), v2FetchFlags(), ), Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if useV2Fetch(runtime) { return validateFetchV2(ctx, runtime) } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { if useV2Fetch(runtime) { return dryRunFetchV2(ctx, runtime) } return dryRunFetchV1(ctx, runtime) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { if useV2Fetch(runtime) { return executeFetchV2(ctx, runtime) } return executeFetchV1(ctx, runtime) }, PostMount: func(cmd *cobra.Command) { installVersionedHelp(cmd, "v1", docsFetchFlagVersions) }, }
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 }, }
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: concatFlags( []common.Flag{ {Name: "api-version", Desc: "API version", Default: "v1", Enum: []string{"v1", "v2"}}, {Name: "doc", Desc: "document URL or token", Required: true}, }, v1UpdateFlags(), v2UpdateFlags(), ), Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if useV2Update(runtime) { return validateUpdateV2(ctx, runtime) } return validateUpdateV1(ctx, runtime) }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { if useV2Update(runtime) { return dryRunUpdateV2(ctx, runtime) } return dryRunUpdateV1(ctx, runtime) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { if useV2Update(runtime) { return executeUpdateV2(ctx, runtime) } return executeUpdateV1(ctx, runtime) }, PostMount: func(cmd *cobra.Command) { installVersionedHelp(cmd, "v1", docsUpdateFlagVersions) }, }
Functions ¶
Types ¶
type UploadDocMediaFileConfig ¶ added in v1.0.18
type UploadDocMediaFileConfig struct {
FilePath string
Reader io.Reader
FileName string
FileSize int64
ParentType string
ParentNode string
DocID string
}
UploadDocMediaFileConfig groups the inputs to uploadDocMediaFile so the call site names each value at call time, avoiding the "8 positional params of mostly string/int64" ambiguity and mirroring the config-struct style already used by DriveMediaUploadAllConfig / DriveMediaMultipartUploadConfig downstream.
Exactly one of FilePath (on-disk source) or Reader (in-memory source for the clipboard flow) should be set. Leave Reader at its zero value (nil interface) when the caller only has FilePath — passing a typed-nil pointer like (*bytes.Reader)(nil) here would make Reader compare non-nil downstream and skip the FilePath open, so the field type is deliberately an interface and the clipboard caller builds it only when it actually has bytes.