Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
var SlidesAddSlide = common.Shortcut{ Service: "slides", Command: "+add-slide", Description: "Add one page to an existing presentation (<img src=\"@./local.png\"> placeholders are auto-uploaded and replaced with file_token)", Risk: "write", Scopes: []string{"slides:presentation:update", "slides:presentation:write_only"}, ConditionalScopes: []string{"wiki:node:read", "docs:document.media:upload"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ requiredPresentationRefFlag(), {Name: "slide", Desc: "one complete <slide> XML document", Required: true, Input: []string{common.File, common.Stdin}}, {Name: "before-slide-id", Desc: "insert before this slide_id (default: append after the last page)"}, {Name: "revision-id", Type: "int", Default: "-1", Desc: "presentation revision (-1 = latest; pass a specific number for optimistic locking)"}, noLintFlag(), }, Tips: []string{ "<img src=\"@path\"> placeholders resolve against the current directory, not the directory of the --slide file, and are deduplicated per call: a page-by-page loop re-uploads a shared image once per page, so upload it once with slides +media-upload and reuse the file_token instead.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } if ref.Kind == "wiki" { if err := runtime.EnsureScopes([]string{"wiki:node:read"}); err != nil { return err } } slideXML, err := addSlideXML(runtime) if err != nil { return err } if err := validateCompleteSlideXML(slideXML); err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--slide is not a single complete <slide> document: %v", err).WithParam("--slide").WithCause(err) } placeholders := extractImagePlaceholderPaths([]string{slideXML}) if len(placeholders) > 0 { if err := runtime.EnsureScopes([]string{"docs:document.media:upload"}); err != nil { return err } if err := validateImagePlaceholderFiles(runtime, "--slide", placeholders); err != nil { return err } } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } slideXML, err := addSlideXML(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } placeholders := extractImagePlaceholderPaths([]string{slideXML}) dry := common.NewDryRunAPI() presentationID := ref.Token step := 1 total := 1 + len(placeholders) if ref.Kind == "wiki" { total++ } if ref.Kind == "wiki" { presentationID = unresolvedSlidesTokenPlaceholder dry.Desc(fmt.Sprintf("%d-step orchestration: resolve wiki → add page", total)). GET("/open-apis/wiki/v2/spaces/get_node"). Desc(fmt.Sprintf("[%d/%d] Resolve wiki node to slides presentation", step, total)). Params(map[string]interface{}{"token": ref.Token}) step++ } else if len(placeholders) > 0 { dry.Desc(fmt.Sprintf("Upload %d image(s) + add 1 page", len(placeholders))) } else { dry.Desc("Add 1 page") } for _, path := range placeholders { appendSlidesUploadDryRun(dry, path, presentationID, slidesDryRunParentType(ref), step) step++ } descSuffix := "" if len(placeholders) > 0 { descSuffix = " (img placeholders auto-replaced)" } dry.POST(fmt.Sprintf( "/open-apis/slides_ai/v1/xml_presentations/%s/slide", validate.EncodePathSegment(presentationID), )). Desc(fmt.Sprintf("[%d/%d] Add page%s", step, total, descSuffix)). Params(addSlideQuery(runtime)). Body(addSlideBody(slideXML, runtime.Str("before-slide-id"), runtime)) return dry.Set("images_to_upload", len(placeholders)) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } presentationID, err := resolvePresentationID(runtime, ref) if err != nil { return err } slideXML, err := addSlideXML(runtime) if err != nil { return err } result := map[string]interface{}{ "xml_presentation_id": presentationID, } placeholders := extractImagePlaceholderPaths([]string{slideXML}) if len(placeholders) > 0 { tokens, uploaded, err := uploadSlidesPlaceholders(runtime, presentationID, placeholders, "--slide") if err != nil { return appendSlidesProgressHint(err, fmt.Sprintf("no page was added; %d of %d image(s) uploaded before failure", uploaded, len(placeholders))) } slideXML = replaceImagePlaceholders(slideXML, tokens) result["images_uploaded"] = uploaded } beforeSlideID := strings.TrimSpace(runtime.Str("before-slide-id")) data, err := runtime.CallAPITyped( "POST", fmt.Sprintf( "/open-apis/slides_ai/v1/xml_presentations/%s/slide", validate.EncodePathSegment(presentationID), ), addSlideQuery(runtime), addSlideBody(slideXML, beforeSlideID, runtime), ) if err != nil { if len(placeholders) > 0 { err = appendSlidesProgressHint(err, fmt.Sprintf("%d image(s) were uploaded before the page failed; re-running will upload them again", len(placeholders))) } return enrichSlidesReplaceError(enrichSlidesLintError(err)) } slideID := common.GetString(data, "slide_id") if slideID == "" { return errs.NewInternalError(errs.SubtypeInvalidResponse, "slide.create returned no slide_id") } result["slide_id"] = slideID if beforeSlideID != "" { result["before_slide_id"] = beforeSlideID } if rev, ok := revisionFromData(data); ok { result["revision_id"] = rev } if issues, ok := data["issues"]; ok { result["issues"] = issues } runtime.Out(result, nil) return nil }, }
SlidesAddSlide appends (or inserts) a single page into an existing presentation. It is the second half of the two-step creation flow: create a blank deck with +create, then add pages one at a time.
Value-adds over the raw xml_presentation.slide.create command:
- --presentation accepts a token / slides URL / wiki URL, like every other slides shortcut, instead of a hand-built --params JSON blob.
- --slide takes the XML directly (and via @file / stdin), so callers stop nesting a fully escaped XML document inside a JSON string inside a shell argument — the escaping layer that produces most 3350001 reports.
- <img src="@./local.png"> placeholders are uploaded and rewritten to file_tokens, the same as +create --slides. Previously this combination had no CLI support at all: adding an image-bearing page to an existing deck meant calling +media-upload and splicing the token in by hand.
Deliberately single-page: the backend endpoint creates one page per call, so a batch flag here would just be a client-side loop with partial-failure semantics to explain. Callers who want many pages loop the command, or use +create --slides when the deck does not exist yet.
var SlidesCreate = common.Shortcut{ Service: "slides", Command: "+create", Description: "Create a Lark Slides presentation", Risk: "write", AuthTypes: []string{"user", "bot"}, Scopes: []string{"slides:presentation:create", "slides:presentation:write_only", "docs:document.media:upload"}, Flags: []common.Flag{ {Name: "title", Desc: "presentation title"}, {Name: "slides", Desc: "slide content JSON array (each element is a <slide> XML string, max 10; for more pages, create first then add them one at a time with slides +add-slide). <img src=\"@./local.png\"> placeholders are auto-uploaded and replaced with file_token.", Input: []string{common.File, common.Stdin}}, {Name: "slide", Type: "string_array", Desc: "one complete <slide> XML document, or @path to read one from a file; repeat once per page (max 10) and the CLI assembles the array for you, so no JSON escaping is needed. <img src=\"@./local.png\"> placeholders are handled as with --slides. Mutually exclusive with --slides."}, noLintFlag(), }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { slides, param, err := createSlideContents(runtime) if err != nil { return err } if len(slides) == 0 { return nil } return validateImagePlaceholderFiles(runtime, param, extractImagePlaceholderPaths(slides)) }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { title := effectiveTitle(runtime.Str("title")) slides, _, err := createSlideContents(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } createBody := map[string]interface{}{ "xml_presentation": map[string]interface{}{"content": buildPresentationXML(title)}, } placeholders := extractImagePlaceholderPaths(slides) botNote := "" if runtime.IsBot() { botNote = " After creation succeeds in bot mode, the CLI will also try to grant the current CLI user full_access on the new presentation." } dry := common.NewDryRunAPI() if len(slides) == 0 { dry.Desc("Create empty presentation"). POST("/open-apis/slides_ai/v1/xml_presentations"). Desc(strings.TrimSpace(botNote)). Body(createBody) } else { n := len(slides) total := n + 1 + len(placeholders) descSuffix := "" if len(placeholders) > 0 { descSuffix = fmt.Sprintf(" + upload %d image(s)", len(placeholders)) } dry.Desc(fmt.Sprintf("Create presentation%s + add %d slide(s)", descSuffix, n)). POST("/open-apis/slides_ai/v1/xml_presentations"). Desc(fmt.Sprintf("[1/%d] Create presentation.%s", total, botNote)). Body(createBody) for i, path := range placeholders { appendSlidesUploadDryRun(dry, path, "<xml_presentation_id>", slideFileParentType, i+2) } slideStepStart := 2 + len(placeholders) slideDescSuffix := "" if len(placeholders) > 0 { slideDescSuffix = " (img placeholders auto-replaced)" } for i, slideXML := range slides { dry.POST("/open-apis/slides_ai/v1/xml_presentations/<xml_presentation_id>/slide"). Desc(fmt.Sprintf("[%d/%d] Add slide %d%s", slideStepStart+i, total, i+1, slideDescSuffix)). Params(createSlideQuery()). Body(createSlideBody(slideXML, runtime)) } } return dry }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { title := effectiveTitle(runtime.Str("title")) content := buildPresentationXML(title) slides, param, err := createSlideContents(runtime) if err != nil { return err } placeholders := extractImagePlaceholderPaths(slides) data, err := runtime.CallAPITyped( "POST", "/open-apis/slides_ai/v1/xml_presentations", nil, map[string]interface{}{ "xml_presentation": map[string]interface{}{ "content": content, }, }, ) if err != nil { return err } presentationID := common.GetString(data, "xml_presentation_id") if presentationID == "" { return errs.NewInternalError(errs.SubtypeInvalidResponse, "slides create returned no xml_presentation_id") } result := map[string]interface{}{ "xml_presentation_id": presentationID, "title": title, } if revisionID := common.GetFloat(data, "revision_id"); revisionID > 0 { result["revision_id"] = int(revisionID) } if issues, ok := data["issues"]; ok { result["issues"] = issues } if len(slides) > 0 { if len(placeholders) > 0 { tokens, uploaded, err := uploadSlidesPlaceholders(runtime, presentationID, placeholders, param) if err != nil { return appendSlidesProgressHint(err, fmt.Sprintf("presentation %s was created; %d image(s) uploaded before failure", presentationID, uploaded)) } for i := range slides { slides[i] = replaceImagePlaceholders(slides[i], tokens) } result["images_uploaded"] = uploaded } slideURL := fmt.Sprintf( "/open-apis/slides_ai/v1/xml_presentations/%s/slide", validate.EncodePathSegment(presentationID), ) var slideIDs []string var slideIssues []map[string]interface{} for i, slideXML := range slides { slideData, err := runtime.CallAPITyped( "POST", slideURL, createSlideQuery(), createSlideBody(slideXML, runtime), ) if err != nil { return appendSlidesProgressHint(enrichSlidesLintError(err), fmt.Sprintf("adding slide %d/%d failed; presentation %s was created, %d slide(s) added before failure", i+1, len(slides), presentationID, i)) } sid := common.GetString(slideData, "slide_id") if sid != "" { slideIDs = append(slideIDs, sid) } if issues, ok := slideData["issues"]; ok { slideIssues = append(slideIssues, map[string]interface{}{ "slide_index": i + 1, "slide_id": sid, "issues": issues, }) } } result["slide_ids"] = slideIDs result["slides_added"] = len(slideIDs) if len(slideIssues) > 0 { result["slide_issues"] = slideIssues } } presentationURL := common.GetString(data, "url") if presentationURL == "" { presentationURL = common.BuildResourceURL(runtime.Config.Brand, "slides", presentationID) } if presentationURL != "" { result["url"] = presentationURL if len(slides) == 0 { result["message"] = fmt.Sprintf("成功创建空白幻灯片,url:%s,请给用户推送开工通知。", presentationURL) } } if grant := common.AutoGrantCurrentUserDrivePermission(runtime, presentationID, "slides"); grant != nil { result["permission_grant"] = grant } runtime.Out(result, nil) return nil }, }
SlidesCreate creates a new Lark Slides presentation with bot auto-grant.
The presentation is created first as an empty shell, then the pages are added one at a time. Each page is linted on its own way in, so a bad page is refused with the findings for that page — and the run stops there, leaving the presentation and the pages added before it. The error says so, so the caller knows what exists and where the run stopped.
var SlidesDeleteSlide = common.Shortcut{ Service: "slides", Command: "+delete-slide", Description: "Delete one page from a presentation by slide_id", Risk: "write", Scopes: []string{"slides:presentation:update", "slides:presentation:write_only"}, ConditionalScopes: []string{"wiki:node:read"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ requiredPresentationRefFlag(), {Name: "slide-id", Desc: "slide page identifier (slide_id) to delete", Required: true}, {Name: "revision-id", Type: "int", Default: "-1", Desc: "presentation revision (-1 = latest; pass a specific number for optimistic locking)"}, }, Tips: []string{ "Deletion is not undoable in place; recover a wrongly deleted page with slides +history-list then +history-revert.", "Use --dry-run to confirm which presentation and slide_id will be hit before running.", }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } if ref.Kind == "wiki" { if err := runtime.EnsureScopes([]string{"wiki:node:read"}); err != nil { return err } } if _, err := deleteSlideID(runtime); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } slideID, err := deleteSlideID(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } dry := common.NewDryRunAPI() presentationID := ref.Token step := 1 total := 1 if ref.Kind == "wiki" { total = 2 presentationID = "<resolved_slides_token>" dry.Desc("2-step orchestration: resolve wiki → delete page"). GET("/open-apis/wiki/v2/spaces/get_node"). Desc("[1/2] Resolve wiki node to slides presentation"). Params(map[string]interface{}{"token": ref.Token}) step = 2 } else { dry.Desc(fmt.Sprintf("Delete page %s", slideID)) } dry.DELETE(fmt.Sprintf( "/open-apis/slides_ai/v1/xml_presentations/%s/slide", validate.EncodePathSegment(presentationID), )). Desc(fmt.Sprintf("[%d/%d] Delete page %s", step, total, slideID)). Params(deleteSlideQuery(runtime, slideID)) return dry.Set("slide_id", slideID) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } presentationID, err := resolvePresentationID(runtime, ref) if err != nil { return err } slideID, err := deleteSlideID(runtime) if err != nil { return err } data, err := runtime.CallAPITyped( "DELETE", fmt.Sprintf( "/open-apis/slides_ai/v1/xml_presentations/%s/slide", validate.EncodePathSegment(presentationID), ), deleteSlideQuery(runtime, slideID), nil, ) if err != nil { return err } result := map[string]interface{}{ "xml_presentation_id": presentationID, "slide_id": slideID, "deleted": true, } if rev, ok := revisionFromData(data); ok { result["revision_id"] = rev } runtime.Out(result, nil) return nil }, }
SlidesDeleteSlide removes a single page from a presentation.
Value-adds over the raw xml_presentation.slide.delete command are the same two every slides shortcut provides: --presentation accepts a token / slides URL / wiki URL, and the identifiers are ordinary flags instead of a hand-escaped --params JSON blob.
Deliberately single-page: deletion is destructive and slide_id lists invite a partial-failure story ("3 of 5 deleted, which 3?"). One page per call keeps the outcome unambiguous.
var SlidesHistoryList = common.Shortcut{ Service: "slides", Command: "+history-list", Description: "List Slides presentation history versions", Risk: "read", Scopes: []string{"slides:presentation:read"}, ConditionalScopes: []string{"wiki:node:read"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ requiredPresentationRefFlag(), {Name: "page-size", Type: "int", Default: "20", Desc: "history entries to return, range 1-20"}, {Name: "page-token", Desc: "pagination token from the previous page's page_token"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if _, err := parseSlidesHistoryPresentation(runtime); err != nil { return err } return validateSlidesHistoryPageSize(runtime.Int("page-size")) }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } spec := slidesHistoryListSpec{ PageSize: runtime.Int("page-size"), PageToken: strings.TrimSpace(runtime.Str("page-token")), } dry, presentationID := newSlidesHistoryDryRun(ref, "list Slides history versions") return dry. GET(slidesHistoryAPIPath(presentationID, "histories")). Params(slidesHistoryListParams(spec)). Set("xml_presentation_id", presentationID) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } presentationID, err := resolvePresentationID(runtime, ref) if err != nil { return err } spec := slidesHistoryListSpec{ PageSize: runtime.Int("page-size"), PageToken: strings.TrimSpace(runtime.Str("page-token")), } data, err := runtime.CallAPITyped( http.MethodGet, slidesHistoryAPIPath(presentationID, "histories"), slidesHistoryListParams(spec), nil, ) if err != nil { return err } runtime.OutRaw(data, nil) return nil }, }
SlidesHistoryList lists history versions of a Slides XML presentation.
var SlidesHistoryRevert = common.Shortcut{ Service: "slides", Command: "+history-revert", Description: "Revert a Slides presentation to a historical version", Risk: "write", Scopes: []string{"slides:presentation:update", "slides:presentation:write_only"}, ConditionalScopes: []string{"wiki:node:read"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ requiredPresentationRefFlag(), {Name: "history-version-id", Desc: "history_version_id from slides +history-list to revert to", Required: true}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if _, err := parseSlidesHistoryPresentation(runtime); err != nil { return err } if err := validateSlidesHistoryVersionID(runtime.Str("history-version-id")); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } spec := slidesHistoryRevertSpec{ HistoryVersionID: strings.TrimSpace(runtime.Str("history-version-id")), } dry, presentationID := newSlidesHistoryDryRun(ref, "revert Slides history") return dry. POST(slidesHistoryAPIPath(presentationID, "history/revert")). Body(slidesHistoryRevertBody(spec)). Set("xml_presentation_id", presentationID) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } presentationID, err := resolvePresentationID(runtime, ref) if err != nil { return err } spec := slidesHistoryRevertSpec{ HistoryVersionID: strings.TrimSpace(runtime.Str("history-version-id")), } data, err := runtime.CallAPITyped( http.MethodPost, slidesHistoryAPIPath(presentationID, "history/revert"), nil, slidesHistoryRevertBody(spec), ) if err != nil { return err } runtime.OutRaw(data, nil) return nil }, }
SlidesHistoryRevert reverts a Slides XML presentation to a history version.
var SlidesHistoryRevertStatus = common.Shortcut{ Service: "slides", Command: "+history-revert-status", Description: "Get Slides history revert task status", Risk: "read", Scopes: []string{"slides:presentation:read"}, ConditionalScopes: []string{"wiki:node:read"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ requiredPresentationRefFlag(), {Name: "task-id", Desc: "task_id returned by slides +history-revert", Required: true}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if _, err := parseSlidesHistoryPresentation(runtime); err != nil { return err } if strings.TrimSpace(runtime.Str("task-id")) == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--task-id is required").WithParam("--task-id") } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } spec := slidesHistoryRevertStatusSpec{ TaskID: strings.TrimSpace(runtime.Str("task-id")), } dry, presentationID := newSlidesHistoryDryRun(ref, "get Slides history revert status") return dry. GET(slidesHistoryAPIPath(presentationID, "history/revert_status")). Params(slidesHistoryStatusParams(spec)). Set("xml_presentation_id", presentationID) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } presentationID, err := resolvePresentationID(runtime, ref) if err != nil { return err } spec := slidesHistoryRevertStatusSpec{ TaskID: strings.TrimSpace(runtime.Str("task-id")), } data, err := runtime.CallAPITyped( http.MethodGet, slidesHistoryAPIPath(presentationID, "history/revert_status"), slidesHistoryStatusParams(spec), nil, ) if err != nil { return err } runtime.OutRaw(data, nil) return nil }, }
SlidesHistoryRevertStatus gets the status of a Slides history revert task.
var SlidesMediaDownload = common.Shortcut{ Service: "slides", Command: "+media-download", Description: "Download a Slides media file_token to a local image file", Risk: "read", Scopes: []string{"docs:document.media:download"}, ConditionalScopes: []string{"drive:file:download"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "file-token", Desc: "Slides media file_token", Required: true}, {Name: "output", Desc: "preferred relative output path for one image (extension optional; .png, .jpg, or .jpeg)"}, {Name: "output-dir", Default: defaultSlidesMediaDownloadDir, Desc: "relative directory for saved media"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if err := validateSlidesMediaDownloadFileToken(runtime.Str("file-token")); err != nil { return err } if runtime.Changed("output") { if runtime.Changed("output-dir") { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--output cannot be combined with --output-dir").WithParam("--output") } return validateScreenshotOutputPath(runtime, runtime.Str("output")) } _, err := validateScreenshotOutputDir(runtime, runtime.Str("output-dir")) return err }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { fileToken := strings.TrimSpace(runtime.Str("file-token")) if err := validateSlidesMediaDownloadFileToken(fileToken); err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } output := runtime.Str("output") if output == "" { output = filepath.Join(runtime.Str("output-dir"), fileToken) } return common.NewDryRunAPI(). Desc("Try direct Drive media download; on permission denied, fetch the source-file preview artifact"). GET(fmt.Sprintf("/open-apis/drive/v1/medias/%s/download", validate.EncodePathSegment(fileToken))). Set("file_token", fileToken). Set("output", output). GET(fmt.Sprintf("/open-apis/drive/v1/medias/%s/preview_download", validate.EncodePathSegment(fileToken))). Desc("Fallback: download the Drive source-file preview artifact"). Params(map[string]interface{}{"preview_type": slidesMediaPreviewTypeSource}). Set("file_token", fileToken) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { fileToken := strings.TrimSpace(runtime.Str("file-token")) if err := validateSlidesMediaDownloadFileToken(fileToken); err != nil { return err } outputTarget, err := resolveSlidesScreenshotOutputTarget(runtime) if err != nil { return err } resp, err := runtime.DoAPIStream(ctx, &larkcore.ApiReq{ HttpMethod: http.MethodGet, ApiPath: fmt.Sprintf("/open-apis/drive/v1/medias/%s/download", validate.EncodePathSegment(fileToken)), }) if err == nil { return saveSlidesMediaDownloadResponse(runtime, resp, outputTarget, fileToken, "download") } if !isSlidesMediaDownloadPermissionError(err) { return wrapSlidesMediaDownloadError(err, "media download failed: %s") } if scopeErr := runtime.EnsureScopes([]string{"drive:file:download"}); scopeErr != nil { return scopeErr } previewResp, previewErr := runtime.DoAPIStream(ctx, &larkcore.ApiReq{ HttpMethod: http.MethodGet, ApiPath: fmt.Sprintf("/open-apis/drive/v1/medias/%s/preview_download", validate.EncodePathSegment(fileToken)), QueryParams: larkcore.QueryParams{ "preview_type": []string{slidesMediaPreviewTypeSource}, }, }) if previewErr != nil { return wrapSlidesMediaDownloadError(previewErr, "source-file preview download failed: %s") } return saveSlidesMediaDownloadResponse(runtime, previewResp, outputTarget, fileToken, "preview") }, }
SlidesMediaDownload downloads a Slides media file token to a local image file. It retries through the Drive source-file preview artifact when direct export is denied, which is the path available for media embedded in Slides.
var SlidesMediaUpload = common.Shortcut{ Service: "slides", Command: "+media-upload", Description: "Upload a local image to a slides presentation and return the file_token (use as <img src=...>)", Risk: "write", Scopes: []string{"docs:document.media:upload", "wiki:node:read"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "file", Desc: "local image path (max 20 MB)", Required: true}, requiredPresentationRefFlag(), }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { if _, err := parsePresentationRef(runtime.Str("presentation")); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { filePath := runtime.Str("file") ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } dry := common.NewDryRunAPI() uploadNode := ref.Token stepBase := 1 if ref.Kind == "wiki" { uploadNode = unresolvedSlidesTokenPlaceholder stepBase = 2 dry.Desc("2-step orchestration: resolve wiki → upload media"). GET("/open-apis/wiki/v2/spaces/get_node"). Desc("[1] Resolve wiki node to slides presentation"). Params(map[string]interface{}{"token": ref.Token}) } else { dry.Desc("Upload local file to slides presentation") } appendSlidesUploadDryRun(dry, filePath, uploadNode, slidesDryRunParentType(ref), stepBase) return dry.Set("presentation_id", ref.Token) }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { filePath := runtime.Str("file") ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } presentationID, err := resolvePresentationID(runtime, ref) if err != nil { return err } stat, err := runtime.FileIO().Stat(filePath) if err != nil { return slidesInputStatError(err, "--file", filePath) } if !stat.Mode().IsRegular() { return errs.NewValidationError(errs.SubtypeInvalidArgument, "file must be a regular file: %s", filePath).WithParam("--file") } if stat.Size() > common.MaxDriveMediaUploadSinglePartSize { return errs.NewValidationError(errs.SubtypeInvalidArgument, "file %s is %s, exceeds 20 MB limit for slides image upload", filepath.Base(filePath), common.FormatSize(stat.Size())).WithParam("--file") } fileName := filepath.Base(filePath) fileToken, err := uploadSlidesMedia(runtime, filePath, fileName, stat.Size(), presentationID) if err != nil { return err } runtime.Out(map[string]interface{}{ "file_token": fileToken, "file_name": fileName, "size": stat.Size(), "presentation_id": presentationID, }, nil) return nil }, }
SlidesMediaUpload uploads a local image to drive media against a slides presentation and returns the file_token. The token can be used as the value of <img src="..."> in slide XML.
This is the atomic building block for getting a local image into a slides deck. Higher-level shortcuts (e.g. +create with @path placeholders) reuse the same upload helpers.
var SlidesReplacePages = common.Shortcut{ Service: "slides", Command: "+replace-pages", Description: "Rebuild multiple pages in a presentation: create each new page before old page, then delete old page (not atomic; changes slide_id and element ids)", Risk: "write", Scopes: []string{"slides:presentation:update", "slides:presentation:write_only"}, ConditionalScopes: []string{"wiki:node:read"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ requiredPresentationRefFlag(), {Name: "pages", Desc: "JSON array of page replacements (each: {slide_id, content}); supports @file or -", Required: true, Input: []string{common.File, common.Stdin}}, {Name: "continue-on-error", Type: "bool", Desc: "continue with later pages after a create/delete failure; default false"}, {Name: "validate-only", Type: "bool", Desc: "validate input and build the create/delete plan without write calls"}, noLintFlag(), }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } if ref.Kind == "wiki" { if err := runtime.EnsureScopes([]string{"wiki:node:read"}); err != nil { return err } } pages, err := parseReplacePages(runtime.Str("pages")) if err != nil { return err } return validateReplacePagesInput(pages) }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { dry := common.NewDryRunAPI() resolved, err := prepareReplacePages(runtime) if err != nil { return dry.Set("error", err.Error()) } appendReplacePagesDryRunCalls(dry, resolved, runtime) return dry. Set("xml_presentation_id", resolved.PresentationID). Set("pages_count", len(resolved.Plan)). Set("plan", replacePagesPlanOutput(resolved.Plan)). Set("note", "dry-run built a create/delete plan from slide_id inputs; no Slides presentation get/create/delete calls were executed") }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { resolved, err := prepareReplacePages(runtime) if err != nil { return err } if runtime.Bool("validate-only") { runtime.Out(map[string]interface{}{ "xml_presentation_id": resolved.PresentationID, "pages_count": len(resolved.Plan), "plan": replacePagesPlanOutput(resolved.Plan), "status": "validated", "note": "validate-only checked input and built the create/delete plan; no Slides presentation get/create/delete calls were executed", }, nil) return nil } revisionID := replacePagesInitialRevisionID results := make([]replacePageResult, 0, len(resolved.Plan)) for i, item := range resolved.Plan { result, err := replaceOnePage(runtime, resolved.PresentationID, item, revisionID) if err != nil { err = enrichSlidesLintError(err) recordReplacePageError(&result, err) } results = append(results, result) if result.RevisionID != nil { revisionID = *result.RevisionID } if err != nil { if runtime.Bool("continue-on-error") { continue } return appendSlidesProgressHint(err, fmt.Sprintf("slides +replace-pages stopped at item %d/%d; %d page(s) completed before failure; old page is kept when create failed", i+1, len(resolved.Plan), countReplacedPages(results))) } } out := map[string]interface{}{ "xml_presentation_id": resolved.PresentationID, "pages_count": len(resolved.Plan), "results": replacePageResultsOutput(results), "status": "completed", "summary": replacePagesSummaryOutput(results), "note": "batch replace is not atomic; each page was created before its old page was deleted", } if revisionID != replacePagesInitialRevisionID { out["revision_id"] = revisionID } if hasReplacePageFailures(results) { out["status"] = "partial_failure" return runtime.OutPartialFailure(out, nil) } runtime.Out(out, nil) return nil }, }
SlidesReplacePages rebuilds multiple pages inside an existing presentation. It deliberately creates the new page before deleting the old one so a create failure cannot remove existing user content. The operation is not atomic.
var SlidesReplaceSlide = common.Shortcut{ Service: "slides", Command: "+replace-slide", Description: "Replace elements on a slide via block_replace / block_insert parts (auto-injects id + <content/> on shape elements)", Risk: "write", Scopes: []string{"slides:presentation:update", "slides:presentation:write_only"}, ConditionalScopes: []string{"wiki:node:read"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ requiredPresentationRefFlag(), {Name: "slide-id", Desc: "slide page identifier (slide_id)", Required: true}, {Name: "parts", Desc: "JSON array of replace parts; accepts replace/insert action aliases, target_id for block_id, and block/content/shape/element for the action's XML payload; max 200", Required: true, Input: []string{common.File, common.Stdin}}, {Name: "revision-id", Type: "int", Default: "-1", Desc: "presentation revision (-1 = latest; pass a specific number for optimistic locking)"}, {Name: "tid", Desc: "transaction id for concurrent-edit locking (usually empty)"}, noLintFlag(), }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } if ref.Kind == "wiki" { if err := runtime.EnsureScopes([]string{"wiki:node:read"}); err != nil { return err } } if strings.TrimSpace(runtime.Str("slide-id")) == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--slide-id cannot be empty").WithParam("--slide-id") } parts, err := parseReplaceParts(runtime.Str("parts")) if err != nil { return err } if err := validateReplaceParts(parts); err != nil { return err } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } parts, normalizations, err := parseReplacePartsWithNormalization(runtime.Str("parts")) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } if err := validateReplaceParts(parts); err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } injected, err := injectBlockReplaceIDs(parts) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } slideID := runtime.Str("slide-id") query := map[string]interface{}{ "slide_id": slideID, "revision_id": runtime.Int("revision-id"), } if tid := runtime.Str("tid"); tid != "" { query["tid"] = tid } body := replaceSlideBody(injected, runtime) dry := common.NewDryRunAPI() presentationID := ref.Token if ref.Kind == "wiki" { presentationID = "<resolved_slides_token>" dry.Desc("2-step orchestration: resolve wiki → replace slide parts"). GET("/open-apis/wiki/v2/spaces/get_node"). Desc("[1] Resolve wiki node to slides presentation"). Params(map[string]interface{}{"token": ref.Token}) } else { dry.Desc(fmt.Sprintf("Replace %d part(s) on slide %s", len(parts), slideID)) } dry.POST(slideReplaceAPIPath(presentationID)). Params(query). Body(body) dry.Set("parts_count", len(parts)) if len(normalizations) > 0 { dry.Set("normalizations", normalizations) } return dry }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } presentationID, err := resolvePresentationID(runtime, ref) if err != nil { return err } slideID := strings.TrimSpace(runtime.Str("slide-id")) parts, normalizations, err := parseReplacePartsWithNormalization(runtime.Str("parts")) if err != nil { return err } if err := validateReplaceParts(parts); err != nil { return err } injected, err := injectBlockReplaceIDs(parts) if err != nil { return err } query := map[string]interface{}{ "slide_id": slideID, "revision_id": runtime.Int("revision-id"), } if tid := strings.TrimSpace(runtime.Str("tid")); tid != "" { query["tid"] = tid } body := replaceSlideBody(injected, runtime) data, err := runtime.CallAPITyped("POST", slideReplaceAPIPath(presentationID), query, body) if err != nil { return enrichSlidesReplaceError(enrichSlidesLintError(err)) } result := map[string]interface{}{ "xml_presentation_id": presentationID, "slide_id": slideID, "parts_count": len(injected), } if len(normalizations) > 0 { result["normalizations"] = normalizations } if _, ok := data["revision_id"]; ok { result["revision_id"] = int(common.GetFloat(data, "revision_id")) } if raw, ok := data["failed_part_index"]; ok { result["failed_part_index"] = raw } if raw, ok := data["failed_reason"]; ok { result["failed_reason"] = raw } if raw, ok := data["issues"]; ok { result["issues"] = raw } runtime.Out(result, nil) return nil }, }
SlidesReplaceSlide wraps slides.xml_presentation.slide.replace with specific value-adds over the raw auto-generated command:
- It accepts --presentation as token / slides URL / wiki URL (and resolves wiki tokens), same as other slides shortcuts.
- For every `block_replace` part it auto-injects `id="<block_id>"` into the root element of `replacement`. The backend requires the replacement fragment's root carry that id and returns 3350001 otherwise; the requirement is undocumented and catches callers repeatedly, so we fix it at the CLI layer.
- For `<shape>` elements it auto-injects `<content/>` when missing. The SML 2.0 schema requires every shape to carry a content child; omitting it triggers 3350001.
- On 3350001 errors it enriches the hint with context-specific guidance so AI agents can self-correct.
- It asks the backend to lint the page these parts produce, and renders the refusal when the lint blocks the write. --no-lint opts out.
`str_replace` is intentionally NOT exposed: product direction is that slide edits go through structural (block-level) operations only. The backend still accepts str_replace, but the CLI rejects it up front.
var SlidesScreenshot = common.Shortcut{ Service: "slides", Command: "+screenshot", Description: "Save up to 10 slide screenshots to local files without printing Base64 image data", Risk: "read", Scopes: []string{"slides:presentation:screenshot"}, ConditionalScopes: []string{"wiki:node:read"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ listModePresentationRefFlag(), {Name: "slide-id", Aliases: []string{"slide-ids", "slides"}, Type: "string_slice", Desc: "slide page identifier (repeat or comma-separated for multiple slides; max 10 pages per request)"}, {Name: "slide-number", Aliases: []string{"slide-numbers"}, Type: "int_array", Desc: "slide page number (repeat for multiple slides; max 10 pages per request)"}, {Name: "slide", Desc: "hidden alias routed to --slide-number for digits, otherwise --slide-id", Hidden: true}, {Name: "content", Desc: "slide XML content to render directly instead of fetching existing slides", Input: []string{common.File, common.Stdin}}, {Name: "output", Desc: "preferred relative output path for a single screenshot (extension optional; .png, .jpg, or .jpeg)"}, {Name: "output-dir", Default: defaultSlidesScreenshotDir, Desc: "relative directory for saved screenshots"}, {Name: "output-name", Desc: "file name stem for --content render output"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { renderMode := runtime.Changed("content") selectorCount := 1 if renderMode { if strings.TrimSpace(runtime.Str("content")) == "" { return slidesScreenshotFlagErrorf("--content cannot be empty") } if slidesScreenshotHasSelectorInput(runtime) { return slidesScreenshotContentSelectorConflictError(runtime) } if runtime.Changed("presentation") { return slidesScreenshotFlagErrorf("--presentation cannot be used with --content") } } else { ref, err := parsePresentationRef(slidesScreenshotPresentation(runtime)) if err != nil { return err } slideIDs, slideNumbers, err := slidesScreenshotSelectors(runtime) if err != nil { return err } if ref.Kind == "wiki" { if err := runtime.EnsureScopes([]string{"wiki:node:read"}); err != nil { return err } } if len(slideIDs) == 0 && len(slideNumbers) == 0 { return slidesScreenshotMissingSelectorError() } selectorCount = len(slideIDs) + len(slideNumbers) if err := validateSlidesScreenshotSelectorLimit(selectorCount); err != nil { return err } } if runtime.Changed("output") { if runtime.Changed("output-dir") { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--output cannot be combined with --output-dir").WithParam("--output") } if runtime.Changed("output-name") { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--output cannot be combined with --output-name").WithParam("--output") } if selectorCount != 1 { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--output requires exactly one slide; use --output-dir for multiple screenshots").WithParam("--output") } if err := validateScreenshotOutputPath(runtime, runtime.Str("output")); err != nil { return err } } else { if !renderMode && runtime.Changed("output-name") { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--output-name is only supported with --content"). WithParam("--output-name"). WithHint("use --output <file> for one existing slide, or --output-dir for multiple slides") } if _, err := validateScreenshotOutputDir(runtime, runtime.Str("output-dir")); err != nil { return err } } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { if runtime.Changed("content") { return dryRunRenderScreenshot(runtime) } ref, err := parsePresentationRef(slidesScreenshotPresentation(runtime)) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } slideIDs, slideNumbers, err := slidesScreenshotSelectors(runtime) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } if err := validateSlidesScreenshotSelectorLimit(len(slideIDs) + len(slideNumbers)); err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } presentationID := ref.Token dry := common.NewDryRunAPI() if ref.Kind == "wiki" { presentationID = "<resolved_slides_token>" dry.Desc("2-step orchestration: resolve wiki → fetch slide screenshot(s)"). GET("/open-apis/wiki/v2/spaces/get_node"). Desc("[1] Resolve wiki node to slides presentation"). Params(map[string]interface{}{"token": ref.Token}) } else { if outputPath := strings.TrimSpace(runtime.Str("output")); outputPath != "" { dry.Desc(fmt.Sprintf("Fetch one slide screenshot and save it as %s", outputPath)) } else { dry.Desc(fmt.Sprintf("Fetch %d slide screenshot(s) and save files under %s", len(slideIDs)+len(slideNumbers), runtime.Str("output-dir"))) } } body := map[string]interface{}{} if len(slideIDs) > 0 { body["slide_ids"] = slideIDs } if len(slideNumbers) > 0 { body["slide_numbers"] = slideNumbers } dry.POST(fmt.Sprintf( "/open-apis/slides_ai/v1/xml_presentations/%s/slide_images", validate.EncodePathSegment(presentationID), )). Body(body) return setSlidesScreenshotDryRunOutput(dry, runtime).Set("base64_output", "suppressed; decoded to local files during execution") }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { if runtime.Changed("content") { return executeRenderScreenshot(runtime) } ref, err := parsePresentationRef(slidesScreenshotPresentation(runtime)) if err != nil { return err } presentationID, err := resolvePresentationID(runtime, ref) if err != nil { return err } slideIDs, slideNumbers, err := slidesScreenshotSelectors(runtime) if err != nil { return err } if len(slideIDs) == 0 && len(slideNumbers) == 0 { return slidesScreenshotMissingSelectorError() } if err := validateSlidesScreenshotSelectorLimit(len(slideIDs) + len(slideNumbers)); err != nil { return err } outputTarget, err := resolveSlidesScreenshotOutputTarget(runtime) if err != nil { return err } url := fmt.Sprintf( "/open-apis/slides_ai/v1/xml_presentations/%s/slide_images", validate.EncodePathSegment(presentationID), ) query := larkcore.QueryParams{} body := map[string]interface{}{} if len(slideIDs) > 0 { body["slide_ids"] = slideIDs } if len(slideNumbers) > 0 { body["slide_numbers"] = slideNumbers } data, err := doSlidesScreenshotAPIJSONWithLogID(runtime, "POST", url, query, body) if err != nil { return enrichSlidesScreenshotSelectorError(err, slideNumbers) } saved, err := saveSlideScreenshots(runtime, data, outputTarget.safeOutputDir, presentationID, outputTarget.requested) if err != nil { return err } result := map[string]interface{}{ "xml_presentation_id": presentationID, "screenshots": saved, } setSlidesScreenshotResultOutput(result, outputTarget, saved) runtime.Out(result, nil) return nil }, }
SlidesScreenshot fetches server-rendered slide screenshots and writes them to local files. The raw API returns Base64 image payloads; this shortcut keeps those payloads out of stdout so agents only see small file metadata.
var SlidesUpdate = func() common.Shortcut { sc := SlidesUpdateSlide sc.Command = "+update" sc.Hidden = true return sc }()
SlidesUpdate registers `slides +update` as a hidden alias.
Agents reach for "slide update" before reading --help, and the command not existing cost them a turn on the error plus a help dump. Accepting the shorter spelling costs nothing; it stays out of --help so the canonical name is the only one advertised.
Derived from the canonical shortcut rather than re-declared, so scopes, identities and flags cannot drift between the two spellings.
var SlidesUpdateSlide = common.Shortcut{ Service: "slides", Command: "+update-slide", Description: "Apply a full <slide> XML to an existing slide, replacing the page in one request (keeps slide_id and page order)", Risk: "write", Scopes: []string{"slides:presentation:update", "slides:presentation:write_only"}, ConditionalScopes: []string{"wiki:node:read", "docs:document.media:upload"}, AuthTypes: []string{"user", "bot"}, Tips: []string{ "Read the page first with `slides +xml-get --slide-id <id>`, edit that XML, hand it back whole", "Anything left out of --content is removed from the page — pass the full page, not a fragment", "Editing one element is cheaper with `slides +replace-slide`", "<img src=\"@path\"> placeholders resolve against the current directory, not the directory of an @file passed to --content, and are deduplicated per call", }, Flags: updateSlideFlags, Validate: updateSlideValidate, DryRun: updateSlideDryRun, Execute: updateSlideExecute, }
SlidesUpdateSlide applies one page of XML to an existing slide.
It sends a single block_replace part whose block_id is the page's own id, so the backend replaces the whole <slide> in one shot: elements the caller kept stay, elements they left out are removed, an element they added appears, and <style> / <note> follow the XML they handed over. Callers describe the page they want instead of enumerating the edits that get them there.
Addressing the page this way needs a backend that accepts the page's own id as block_id. Against one that does not, every call fails at the API, not in the CLI — there is no client-side fallback here on purpose: splitting a page into element-level parts means guessing which of the two XML normalizations, the caller's or the server's, counts as "unchanged".
Related: `slides +replace-slide` still takes explicit element-level parts and remains the cheaper call when only one element changes.
var SlidesXMLGet = common.Shortcut{ Service: "slides", Command: "+xml-get", Description: "Fetch presentation XML or one slide XML", Risk: "read", Scopes: []string{"slides:presentation:read"}, ConditionalScopes: []string{"wiki:node:read"}, AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ requiredPresentationRefFlag(), {Name: "output", Desc: "local XML output path; must be a relative path within the current directory; existing file is overwritten; omit to return XML in the JSON envelope"}, {Name: "raw", Type: "bool", Desc: "print raw XML to stdout instead of the JSON envelope; incompatible with --output and --jq"}, {Name: "slide-id", Desc: "slide page identifier; omit both slide selectors to fetch full presentation XML"}, {Name: "slide-number", Type: "int", Desc: "1-based slide page number; omit both slide selectors to fetch full presentation XML"}, {Name: "revision-id", Type: "int", Default: "-1", Desc: "presentation revision_id; -1 means latest"}, {Name: "remove-attr-id", Type: "bool", Desc: "remove XML id attributes in the returned content; useful for read-only inspection, not precise block editing"}, }, Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } if revisionID := runtime.Int("revision-id"); revisionID < -1 { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--revision-id must be -1 or a non-negative integer").WithParam("--revision-id") } if ref.Kind == "wiki" { if err := runtime.EnsureScopes([]string{"wiki:node:read"}); err != nil { return err } } if err := validateSlidesXMLGetSelector(runtime); err != nil { return err } outputPath := strings.TrimSpace(runtime.Str("output")) if outputPath != "" { if _, err := runtime.ResolveSavePath(outputPath); err != nil { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--output invalid: %v", err).WithParam("--output").WithCause(err) } } if runtime.Bool("raw") { if outputPath != "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--raw cannot be used with --output").WithParam("--raw") } if runtime.JqExpr != "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--raw cannot be used with --jq").WithParam("--raw") } if runtime.Changed("format") && runtime.Format != "json" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--raw cannot be used with --format %s", runtime.Format).WithParam("--raw") } } return nil }, DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) } presentationID := ref.Token dry := common.NewDryRunAPI() if ref.Kind == "wiki" { presentationID = "<resolved_slides_token>" dry.Desc("2-step orchestration: resolve wiki → fetch presentation XML"). GET("/open-apis/wiki/v2/spaces/get_node"). Desc("[1] Resolve wiki node to slides presentation"). Params(map[string]interface{}{"token": ref.Token}) } else { dry.Desc("Fetch presentation XML") } params := map[string]interface{}{ "revision_id": runtime.Int("revision-id"), } slideID := strings.TrimSpace(runtime.Str("slide-id")) slideNumber := runtime.Int("slide-number") if slideID != "" { params["slide_id"] = slideID } if slideNumber > 0 { params["slide_number"] = slideNumber } if slideID == "" && slideNumber == 0 && runtime.Bool("remove-attr-id") { params["remove_attr_id"] = true } path := fmt.Sprintf("/open-apis/slides_ai/v1/xml_presentations/%s", validate.EncodePathSegment(presentationID)) if slideID != "" || slideNumber > 0 { path += "/slide" } dry.GET(path).Params(params) if outputPath := strings.TrimSpace(runtime.Str("output")); outputPath != "" { return dry.Set("output", outputPath).Set("stdout_content", "suppressed; XML content is saved to --output during execution") } if runtime.Bool("raw") { return dry.Set("output", "<stdout>").Set("stdout_content", "raw XML content is printed to stdout during execution") } return dry.Set("output", "<stdout>").Set("stdout_content", "JSON envelope with XML content is printed to stdout during execution") }, Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { ref, err := parsePresentationRef(runtime.Str("presentation")) if err != nil { return err } presentationID, err := resolvePresentationID(runtime, ref) if err != nil { return err } if err := validateSlidesXMLGetSelector(runtime); err != nil { return err } params := map[string]interface{}{ "revision_id": runtime.Int("revision-id"), } slideID := strings.TrimSpace(runtime.Str("slide-id")) slideNumber := runtime.Int("slide-number") content, out, err := fetchSlidesXMLGetContent(runtime, presentationID, params, slideID, slideNumber) if err != nil { return err } outputPath := strings.TrimSpace(runtime.Str("output")) return outputSlidesXMLGetContent(runtime, content, outputPath, out) }, }
SlidesXMLGet fetches the full XML presentation content. When --output is provided it writes to a local file; otherwise it returns the XML in the standard JSON envelope. Use --slide-id or --slide-number to fetch one page, and use --raw for direct XML stdout.
Functions ¶
Types ¶
This section is empty.
Source Files
¶
- helpers.go
- presentation_flag.go
- shortcuts.go
- slides_add_slide.go
- slides_create.go
- slides_delete_slide.go
- slides_errors.go
- slides_history.go
- slides_lint_error.go
- slides_lint_param.go
- slides_media_download.go
- slides_media_upload.go
- slides_replace_pages.go
- slides_replace_slide.go
- slides_screenshot.go
- slides_shared.go
- slides_update_slide.go
- slides_xml_get.go