Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
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.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.
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.
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", "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 { if _, err := parsePresentationRef(runtime.Str("presentation")); 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:
- 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.
`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 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{ {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.
Functions ¶
Types ¶
This section is empty.