From ce0ba1f0fe62e33f7aece12a5fb193a4e83db123 Mon Sep 17 00:00:00 2001 From: "songtianyi.theo" Date: Thu, 9 Jul 2026 02:42:29 +0800 Subject: [PATCH] feat: add slides create-svglide shortcut --- shortcuts/slides/shortcuts.go | 1 + shortcuts/slides/slides_create_svglide.go | 164 ++++++++++++++++++ .../slides/slides_create_svglide_test.go | 100 +++++++++++ 3 files changed, 265 insertions(+) create mode 100644 shortcuts/slides/slides_create_svglide.go diff --git a/shortcuts/slides/shortcuts.go b/shortcuts/slides/shortcuts.go index 011e27e0b..3a15cdffd 100644 --- a/shortcuts/slides/shortcuts.go +++ b/shortcuts/slides/shortcuts.go @@ -9,6 +9,7 @@ import "github.com/larksuite/cli/shortcuts/common" func Shortcuts() []common.Shortcut { return []common.Shortcut{ SlidesCreate, + SlidesCreateSVGlide, SlidesMediaUpload, SlidesReplaceSlide, SlidesReplacePages, diff --git a/shortcuts/slides/slides_create_svglide.go b/shortcuts/slides/slides_create_svglide.go new file mode 100644 index 000000000..4e7da8a2f --- /dev/null +++ b/shortcuts/slides/slides_create_svglide.go @@ -0,0 +1,164 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package slides + +import ( + "context" + "fmt" + + "github.com/larksuite/cli/errs" + "github.com/larksuite/cli/internal/validate" + "github.com/larksuite/cli/shortcuts/common" +) + +// SlidesCreateSVGlide publishes a validator-ready SVGlide manifest as raw SVG pages. +var SlidesCreateSVGlide = common.Shortcut{ + Service: "slides", + Command: "+create-svglide", + Description: "Create a Lark Slides presentation from an existing SVGlide manifest", + Risk: "write", + AuthTypes: []string{"user", "bot"}, + Scopes: []string{"slides:presentation:create", "slides:presentation:write_only", "docs:document.media:upload"}, + Flags: []common.Flag{ + {Name: "manifest", Desc: "SVGlide manifest JSON file; version=svglide.manifest.v1, max 10 pages", Required: true}, + {Name: "title", Desc: "presentation title override; defaults to manifest.title, then Untitled"}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + spec, err := prepareSVGlidePublishSpec(runtime.Str("manifest"), runtime.Str("title")) + if err != nil { + return err + } + _, err = planSVGlideImageRewrites(runtime, spec) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + spec, err := prepareSVGlidePublishSpec(runtime.Str("manifest"), runtime.Str("title")) + if err != nil { + return common.NewDryRunAPI().Set("error", err.Error()) + } + imagePlan, err := planSVGlideImageRewrites(runtime, spec) + if err != nil { + return common.NewDryRunAPI().Set("error", err.Error()) + } + total := 1 + len(imagePlan.UniqueFiles) + len(spec.Pages) + dry := common.NewDryRunAPI() + dry.Desc(fmt.Sprintf("Create SVGlide presentation + add %d page(s)", len(spec.Pages))). + POST("/open-apis/slides_ai/v1/xml_presentations"). + Desc(fmt.Sprintf("[1/%d] Create presentation", total)). + Body(map[string]interface{}{ + "xml_presentation": map[string]interface{}{"content": buildPresentationXML(spec.Title)}, + }) + for i, file := range imagePlan.UniqueFiles { + appendSlidesUploadDryRun(dry, file.FilePath, "", i+2) + } + pageStepStart := 2 + len(imagePlan.UniqueFiles) + for i, page := range spec.Pages { + dry.POST("/open-apis/slides_ai/v1/xml_presentations//slide"). + Desc(fmt.Sprintf("[%d/%d] Add SVGlide page %d (%s)", pageStepStart+i, total, i+1, page.ID)). + Params(map[string]interface{}{"revision_id": -1}). + Body(map[string]interface{}{ + "slide": map[string]interface{}{ + "content": fmt.Sprintf("", page.SHA256, page.SourceBytes), + "content_type": "svg", + "source_bytes": page.SourceBytes, + "source_sha256": page.SHA256, + "submitted_bytes": "", + "submitted_sha256": "", + "content_omitted": true, + }, + }) + } + 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.Set("manifest", runtime.Str("manifest")).Set("svglide_manifest_version", svglideManifestVersion) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + spec, err := prepareSVGlidePublishSpec(runtime.Str("manifest"), runtime.Str("title")) + if err != nil { + return err + } + imagePlan, err := planSVGlideImageRewrites(runtime, spec) + if err != nil { + return err + } + data, err := runtime.CallAPITyped( + "POST", + "/open-apis/slides_ai/v1/xml_presentations", + nil, + map[string]interface{}{ + "xml_presentation": map[string]interface{}{"content": buildPresentationXML(spec.Title)}, + }, + ) + if err != nil { + return err + } + presentationID := common.GetString(data, "xml_presentation_id") + if presentationID == "" { + return errs.NewInternalError(errs.SubtypeInvalidResponse, "slides create-svglide returned no xml_presentation_id") + } + + submitPages, imageRewrite, err := uploadAndRewriteSVGlideImages(runtime, spec, imagePlan, presentationID) + if err != nil { + return appendSlidesProgressHint(err, fmt.Sprintf("uploading SVGlide image assets failed; presentation %s was created, no pages were added", presentationID)) + } + + result := map[string]interface{}{ + "xml_presentation_id": presentationID, + "title": spec.Title, + "manifest": spec.ManifestPath, + "svglide_manifest_version": svglideManifestVersion, + "images_uploaded": imageRewrite.UploadedCount, + "image_rewrites": imageRewrite.Entries, + } + if revisionID := common.GetFloat(data, "revision_id"); revisionID > 0 { + result["revision_id"] = int(revisionID) + } + + slideURL := fmt.Sprintf("/open-apis/slides_ai/v1/xml_presentations/%s/slide", validate.EncodePathSegment(presentationID)) + slideIDs := make([]string, 0, len(submitPages)) + pageResults := make([]map[string]interface{}, 0, len(submitPages)) + for i, page := range submitPages { + slideData, err := runtime.CallAPITyped( + "POST", + slideURL, + map[string]interface{}{"revision_id": -1}, + map[string]interface{}{ + "slide": map[string]interface{}{"content": page.Content}, + }, + ) + if err != nil { + return appendSlidesProgressHint(err, fmt.Sprintf("adding SVGlide page %d/%d (%s) failed; presentation %s was created, %d page(s) added before failure", i+1, len(submitPages), page.ID, presentationID, i)) + } + slideID := common.GetString(slideData, "slide_id") + if slideID != "" { + slideIDs = append(slideIDs, slideID) + } + pageResults = append(pageResults, map[string]interface{}{ + "id": page.ID, + "index": page.Index, + "file": page.File, + "source_sha256": page.SHA256, + "source_bytes": page.SourceBytes, + "submitted_sha256": sha256Hex([]byte(page.Content)), + "submitted_bytes": page.ContentBytes, + "slide_id": slideID, + }) + } + + result["slide_ids"] = slideIDs + result["slides_added"] = len(slideIDs) + result["pages"] = pageResults + 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 + }, +} diff --git a/shortcuts/slides/slides_create_svglide_test.go b/shortcuts/slides/slides_create_svglide_test.go index b3aa59d00..91bcb0a0d 100644 --- a/shortcuts/slides/slides_create_svglide_test.go +++ b/shortcuts/slides/slides_create_svglide_test.go @@ -5,6 +5,7 @@ package slides import ( "context" + "encoding/json" "errors" "fmt" "os" @@ -324,6 +325,105 @@ func TestUploadAndRewriteSVGlideImagesUploadsOnceAndRewrites(t *testing.T) { reg.Verify(t) } +func TestSlidesCreateSVGlideExecute(t *testing.T) { + dir := t.TempDir() + withSlidesTestWorkingDir(t, dir) + page := `` + mustWriteSVGlideBundle(t, "SVGlide Deck", page) + + f, stdout, _, reg := cmdutil.TestFactory(t, slidesTestConfig(t, "")) + reg.Register(&httpmock.Stub{ + Method: "POST", + URL: "/open-apis/slides_ai/v1/xml_presentations", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "xml_presentation_id": "pres_sg", + "revision_id": 1, + "url": "https://tenant.example.com/slides/pres_sg", + }, + }, + }) + slideStub := &httpmock.Stub{ + Method: "POST", + URL: "/open-apis/slides_ai/v1/xml_presentations/pres_sg/slide", + Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"slide_id": "slide_1", "revision_id": 2}}, + } + reg.Register(slideStub) + + err := runSlidesShortcut(t, f, stdout, SlidesCreateSVGlide, []string{ + "+create-svglide", + "--manifest", "manifest.json", + "--as", "user", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + data := decodeSlidesCreateEnvelope(t, stdout) + if data["xml_presentation_id"] != "pres_sg" { + t.Fatalf("xml_presentation_id = %v", data["xml_presentation_id"]) + } + if data["slides_added"] != float64(1) { + t.Fatalf("slides_added = %v, want 1", data["slides_added"]) + } + if data["svglide_manifest_version"] != svglideManifestVersion { + t.Fatalf("svglide_manifest_version = %v", data["svglide_manifest_version"]) + } + + var body map[string]interface{} + if err := json.Unmarshal(slideStub.CapturedBody, &body); err != nil { + t.Fatalf("decode slide body: %v", err) + } + slide := body["slide"].(map[string]interface{}) + if !strings.Contains(slide["content"].(string), "` + mustWriteSVGlideBundle(t, "SVGlide Deck", page) + + f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, "")) + err := runSlidesShortcut(t, f, stdout, SlidesCreateSVGlide, []string{ + "+create-svglide", + "--manifest", "manifest.json", + "--dry-run", + "--as", "user", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + out := stdout.String() + for _, want := range []string{"Create SVGlide presentation", "Add SVGlide page 1", "source_sha256", "submitted_sha256", "/open-apis/slides_ai/v1/xml_presentations", "/slide"} { + if !strings.Contains(out, want) { + t.Fatalf("dry-run missing %q: %s", want, out) + } + } + if strings.Contains(out, "