mirror of
https://github.com/larksuite/cli.git
synced 2026-07-09 18:34:03 +08:00
feat: add slides create-svglide shortcut
This commit is contained in:
@@ -9,6 +9,7 @@ import "github.com/larksuite/cli/shortcuts/common"
|
||||
func Shortcuts() []common.Shortcut {
|
||||
return []common.Shortcut{
|
||||
SlidesCreate,
|
||||
SlidesCreateSVGlide,
|
||||
SlidesMediaUpload,
|
||||
SlidesReplaceSlide,
|
||||
SlidesReplacePages,
|
||||
|
||||
164
shortcuts/slides/slides_create_svglide.go
Normal file
164
shortcuts/slides/slides_create_svglide.go
Normal file
@@ -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, "<xml_presentation_id>", i+2)
|
||||
}
|
||||
pageStepStart := 2 + len(imagePlan.UniqueFiles)
|
||||
for i, page := range spec.Pages {
|
||||
dry.POST("/open-apis/slides_ai/v1/xml_presentations/<xml_presentation_id>/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("<raw SVG omitted: source_sha256=%s source_bytes=%d>", page.SHA256, page.SourceBytes),
|
||||
"content_type": "svg",
|
||||
"source_bytes": page.SourceBytes,
|
||||
"source_sha256": page.SHA256,
|
||||
"submitted_bytes": "<computed after image upload>",
|
||||
"submitted_sha256": "<computed after image upload>",
|
||||
"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
|
||||
},
|
||||
}
|
||||
@@ -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 := `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="page-001" viewBox="0 0 960 540"><rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/></svg>`
|
||||
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), "<svg") {
|
||||
t.Fatalf("slide content did not contain SVGlide payload: %#v", slide["content"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidesCreateSVGlideDryRun(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
withSlidesTestWorkingDir(t, dir)
|
||||
page := `<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540"><rect/></svg>`
|
||||
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, "<svg") {
|
||||
t.Fatalf("dry-run must summarize SVG content instead of printing full payload: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func mustWriteSVGlideBundle(t *testing.T, title string, page string) {
|
||||
t.Helper()
|
||||
mustWriteSVGlideTestFile(t, "pages/page-001.svg", page)
|
||||
mustWriteSVGlideTestFile(t, "receipts/validate_svg_deck.json", `{"totalErrors":0}`)
|
||||
mustWriteSVGlideTestFile(t, "manifest.json", fmt.Sprintf(`{
|
||||
"version": "svglide.manifest.v1",
|
||||
"protocol": "svg-slides.v1",
|
||||
"title": %q,
|
||||
"size": {"width": 960, "height": 540},
|
||||
"publish_ready": true,
|
||||
"published": false,
|
||||
"pages": [{"id": "page-001", "index": 1, "file": "pages/page-001.svg", "sha256": %q}],
|
||||
"receipts": {"validate_svg_deck": "receipts/validate_svg_deck.json"}
|
||||
}`, title, sha256Hex([]byte(page))))
|
||||
}
|
||||
|
||||
func newSVGlideTestRuntime(t *testing.T) (*common.RuntimeContext, *httpmock.Registry) {
|
||||
t.Helper()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
Reference in New Issue
Block a user