slides

package
v1.0.62 Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
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 via xml_presentation.slide.create). <img src=\"@./local.png\"> placeholders are auto-uploaded and replaced with file_token."},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if slidesStr := runtime.Str("slides"); slidesStr != "" {
			var slides []string
			if err := json.Unmarshal([]byte(slidesStr), &slides); err != nil {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--slides invalid JSON, must be an array of XML strings").WithParam("--slides")
			}
			if len(slides) > maxSlidesPerCreate {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--slides array exceeds maximum of %d slides; create the presentation first, then add slides via xml_presentation.slide.create", maxSlidesPerCreate).WithParam("--slides")
			}

			for _, path := range extractImagePlaceholderPaths(slides) {
				stat, err := runtime.FileIO().Stat(path)
				if err != nil {
					return slidesInputStatError(err, "--slides", fmt.Sprintf("--slides @%s: file not found", path))
				}
				if !stat.Mode().IsRegular() {
					return errs.NewValidationError(errs.SubtypeInvalidArgument, "--slides @%s: must be a regular file", path).WithParam("--slides")
				}
				if stat.Size() > common.MaxDriveMediaUploadSinglePartSize {
					return errs.NewValidationError(errs.SubtypeInvalidArgument, "--slides @%s: file size %s exceeds 20 MB limit for slides image upload",
						path, common.FormatSize(stat.Size())).WithParam("--slides")
				}
			}
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		title := effectiveTitle(runtime.Str("title"))
		slidesStr := runtime.Str("slides")
		createBody := map[string]interface{}{
			"xml_presentation": map[string]interface{}{"content": buildPresentationXML(title)},
		}

		dry := common.NewDryRunAPI()

		if slidesStr == "" {
			dry.Desc("Create empty presentation").
				POST("/open-apis/slides_ai/v1/xml_presentations").
				Body(createBody)
		} else {
			var slides []string
			_ = json.Unmarshal([]byte(slidesStr), &slides)
			n := len(slides)
			placeholders := extractImagePlaceholderPaths(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", total)).
				Body(createBody)

			for i, path := range placeholders {
				appendSlidesUploadDryRun(dry, path, "<xml_presentation_id>", 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)).
					Body(map[string]interface{}{
						"slide": map[string]interface{}{"content": slideXML},
					})
			}
		}

		if runtime.IsBot() {
			dry.Desc("After creation succeeds in bot mode, the CLI will also try to grant the current CLI user full_access (可管理权限) on the new presentation.")
		}
		return dry
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		title := effectiveTitle(runtime.Str("title"))
		content := buildPresentationXML(title)
		slidesStr := runtime.Str("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 slidesStr != "" {
			var slides []string
			_ = json.Unmarshal([]byte(slidesStr), &slides)

			if len(slides) > 0 {

				placeholders := extractImagePlaceholderPaths(slides)
				if len(placeholders) > 0 {
					tokens, uploaded, err := uploadSlidesPlaceholders(runtime, presentationID, placeholders)
					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
				for i, slideXML := range slides {
					slideData, err := runtime.CallAPITyped(
						"POST",
						slideURL,
						map[string]interface{}{"revision_id": -1},
						map[string]interface{}{
							"slide": map[string]interface{}{"content": slideXML},
						},
					)
					if err != nil {
						return appendSlidesProgressHint(err, fmt.Sprintf("adding slide %d/%d failed; presentation %s was created, %d slide(s) added before failure", i+1, len(slides), presentationID, i))
					}
					if sid := common.GetString(slideData, "slide_id"); sid != "" {
						slideIDs = append(slideIDs, sid)
					}
				}

				result["slide_ids"] = slideIDs
				result["slides_added"] = len(slideIDs)
			}
		}

		if url := common.GetString(data, "url"); url != "" {
			result["url"] = url
		} else if url := common.BuildResourceURL(runtime.Config.Brand, "slides", presentationID); url != "" {
			result["url"] = url
		}

		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.

View Source
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},
		{Name: "presentation", Desc: "xml_presentation_id, slides URL, or wiki URL that resolves to slides", Required: true},
	},
	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()
		parentNode := ref.Token
		stepBase := 1
		if ref.Kind == "wiki" {
			parentNode = "<resolved_slides_token>"
			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, parentNode, 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", "file not found")
		}
		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)
		fmt.Fprintf(runtime.IO().ErrOut, "Uploading: %s (%s) -> presentation %s\n",
			fileName, common.FormatSize(stat.Size()), common.MaskToken(presentationID))

		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.

View Source
var SlidesReplacePages = common.Shortcut{
	Service:     "slides",
	Command:     "+replace-pages",
	Description: "Batch rebuild pages inside an existing Slides presentation (create before old page, then delete old page; not atomic)",
	Risk:        "write",
	Scopes:      []string{"slides:presentation:update", "slides:presentation:write_only"},

	ConditionalScopes: []string{"wiki:node:read"},
	AuthTypes:         []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "presentation", Desc: "xml_presentation_id, slides URL, or wiki URL that resolves to slides", Required: true},
		{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"},
	},
	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)
		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)
			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
	},
}
View Source
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{
		{Name: "presentation", Desc: "xml_presentation_id, slides URL, or wiki URL that resolves to slides", Required: true},
		{Name: "slide-id", Desc: "slide page identifier (slide_id)", Required: true},
		{Name: "parts", Desc: "JSON array of replace parts (each: {action: block_replace|block_insert, ...}); 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)"},
	},
	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, err := parseReplaceParts(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 := map[string]interface{}{"parts": injected}

		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(fmt.Sprintf(
			"/open-apis/slides_ai/v1/xml_presentations/%s/slide/replace",
			validate.EncodePathSegment(presentationID),
		)).
			Params(query).
			Body(body)
		return dry.Set("parts_count", len(parts))
	},
	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, err := parseReplaceParts(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 := map[string]interface{}{"parts": injected}

		url := fmt.Sprintf(
			"/open-apis/slides_ai/v1/xml_presentations/%s/slide/replace",
			validate.EncodePathSegment(presentationID),
		)
		data, err := runtime.CallAPITyped("POST", url, query, body)
		if err != nil {
			return enrichSlidesReplaceError(err)
		}

		result := map[string]interface{}{
			"xml_presentation_id": presentationID,
			"slide_id":            slideID,
			"parts_count":         len(injected),
		}

		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
		}

		runtime.Out(result, nil)
		return nil
	},
}

SlidesReplaceSlide wraps slides.xml_presentation.slide.replace with specific value-adds over the raw auto-generated command:

  1. It accepts --presentation as token / slides URL / wiki URL (and resolves wiki tokens), same as other slides shortcuts.
  2. 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.
  3. 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.
  4. On 3350001 errors it enriches the hint with context-specific guidance so AI agents can self-correct.

`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.

View Source
var SlidesScreenshot = common.Shortcut{
	Service:     "slides",
	Command:     "+screenshot",
	Description: "Save slide screenshots to local files without printing Base64 image data",
	Risk:        "read",
	Scopes:      []string{},

	ConditionalScopes: []string{"wiki:node:read"},
	AuthTypes:         []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "presentation", Desc: "xml_presentation_id, slides URL, or wiki URL that resolves to slides; list mode only"},
		{Name: "slide-id", Type: "string_array", Desc: "slide page identifier (repeat for multiple slides)"},
		{Name: "slide-number", Type: "int_array", Desc: "slide page number (repeat for multiple slides)"},
		{Name: "content", Desc: "slide XML content to render directly instead of fetching existing slides", Input: []string{common.File, common.Stdin}},
		{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")
		if renderMode {
			if strings.TrimSpace(runtime.Str("content")) == "" {
				return slidesScreenshotFlagErrorf("--content cannot be empty")
			}
			if len(normalizeSlideIDs(runtime.StrArray("slide-id"))) > 0 || len(runtime.IntArray("slide-number")) > 0 {
				return slidesScreenshotFlagErrorf("--content cannot be used with --slide-id or --slide-number")
			}
			if runtime.Changed("presentation") {
				return slidesScreenshotFlagErrorf("--presentation cannot be used with --content")
			}
		} else {
			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 := normalizeSlideNumbers(runtime.IntArray("slide-number")); err != nil {
				return err
			}
			if !hasSlideScreenshotSelector(runtime) {
				return slidesScreenshotFlagErrorf("--slide-id or --slide-number is required")
			}
		}
		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(runtime.Str("presentation"))
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}
		slideIDs := normalizeSlideIDs(runtime.StrArray("slide-id"))
		slideNumbers, err := normalizeSlideNumbers(runtime.IntArray("slide-number"))
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}
		if len(slideIDs) == 0 && len(slideNumbers) == 0 {
			return common.NewDryRunAPI().Set("error", "--slide-id or --slide-number is required")
		}

		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 {
			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 dry.Set("output_dir", runtime.Str("output-dir")).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(runtime.Str("presentation"))
		if err != nil {
			return err
		}
		presentationID, err := resolvePresentationID(runtime, ref)
		if err != nil {
			return err
		}

		slideIDs := normalizeSlideIDs(runtime.StrArray("slide-id"))
		slideNumbers, err := normalizeSlideNumbers(runtime.IntArray("slide-number"))
		if err != nil {
			return err
		}
		if len(slideIDs) == 0 && len(slideNumbers) == 0 {
			return slidesScreenshotFlagErrorf("--slide-id or --slide-number is required")
		}
		outputDir := runtime.Str("output-dir")
		safeOutputDir, err := ensureScreenshotOutputDir(runtime, outputDir)
		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, safeOutputDir, presentationID)
		if err != nil {
			return err
		}
		runtime.Out(map[string]interface{}{
			"xml_presentation_id": presentationID,
			"output_dir":          outputDir,
			"screenshots":         saved,
		}, 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.

View Source
var SlidesXMLGet = common.Shortcut{
	Service:     "slides",
	Command:     "+xml-get",
	Description: "Fetch full presentation XML and save it to a local file",
	Risk:        "read",
	Scopes:      []string{"slides:presentation:read"},

	ConditionalScopes: []string{"wiki:node:read"},
	AuthTypes:         []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "presentation", Desc: "xml_presentation_id, slides URL, or wiki URL that resolves to slides", Required: true},
		{Name: "output", Desc: "local XML output path; existing file is overwritten", Required: true},
		{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 ref.Kind == "wiki" {
			if err := runtime.EnsureScopes([]string{"wiki:node:read"}); err != nil {
				return err
			}
		}
		if strings.TrimSpace(runtime.Str("output")) == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--output cannot be empty").WithParam("--output")
		}
		if _, err := runtime.ResolveSavePath(runtime.Str("output")); err != nil {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--output invalid: %v", err).WithParam("--output").WithCause(err)
		}
		if runtime.Int("revision-id") < -1 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--revision-id must be -1 or a non-negative integer").WithParam("--revision-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())
		}
		presentationID := ref.Token
		dry := common.NewDryRunAPI()
		if ref.Kind == "wiki" {
			presentationID = "<resolved_slides_token>"
			dry.Desc("2-step orchestration: resolve wiki → fetch full 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 full presentation XML and save it to a local file")
		}
		params := map[string]interface{}{
			"revision_id": runtime.Int("revision-id"),
		}
		if runtime.Bool("remove-attr-id") {
			params["remove_attr_id"] = true
		}
		dry.GET(fmt.Sprintf(
			"/open-apis/slides_ai/v1/xml_presentations/%s",
			validate.EncodePathSegment(presentationID),
		)).
			Params(params)
		return dry.Set("output", runtime.Str("output")).Set("stdout_content", "suppressed; XML content is saved to --output 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
		}

		params := map[string]interface{}{
			"revision_id": runtime.Int("revision-id"),
		}
		if runtime.Bool("remove-attr-id") {
			params["remove_attr_id"] = true
		}
		data, err := runtime.CallAPITyped(
			"GET",
			fmt.Sprintf("/open-apis/slides_ai/v1/xml_presentations/%s", validate.EncodePathSegment(presentationID)),
			params,
			nil,
		)
		if err != nil {
			return err
		}

		presentation := common.GetMap(data, "xml_presentation")
		content := common.GetString(presentation, "content")
		if content == "" {
			return errs.NewInternalError(errs.SubtypeInvalidResponse, "slides xml get returned empty xml_presentation.content")
		}
		outputPath := runtime.Str("output")
		result, err := runtime.FileIO().Save(outputPath, fileio.SaveOptions{
			ContentType:   "application/xml",
			ContentLength: int64(len(content)),
		}, bytes.NewReader([]byte(content)))
		if err != nil {
			return common.WrapSaveErrorTyped(err)
		}
		resolvedPath, err := runtime.ResolveSavePath(outputPath)
		if err != nil {
			return errs.NewInternalError(errs.SubtypeFileIO, "resolve saved XML path %s: %v", outputPath, err).WithCause(err)
		}

		out := map[string]interface{}{
			"xml_presentation_id": presentationID,
			"path":                resolvedPath,
			"size":                result.Size(),
			"content_saved":       true,
		}
		if revisionID := common.GetFloat(presentation, "revision_id"); revisionID > 0 {
			out["revision_id"] = int(revisionID)
		}
		if runtime.Bool("remove-attr-id") {
			out["remove_attr_id"] = true
		}
		runtime.Out(out, nil)
		return nil
	},
}

SlidesXMLGet fetches the full XML presentation content and writes it to a local file, keeping the terminal output small for large decks.

Functions

func Shortcuts

func Shortcuts() []common.Shortcut

Shortcuts returns all slides shortcuts.

Types

This section is empty.

Jump to

Keyboard shortcuts

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