From da7e954dc958e7b813fa94932e21eeb72e28ff56 Mon Sep 17 00:00:00 2001 From: zhanghuanxu Date: Wed, 8 Jul 2026 02:16:18 +0800 Subject: [PATCH] fix: limit slides screenshot page requests --- shortcuts/slides/slides_screenshot.go | 33 ++++++++++++++----- shortcuts/slides/slides_screenshot_test.go | 27 +++++++++++++++ skills/lark-slides/SKILL.md | 2 +- .../references/lark-slides-screenshot.md | 11 ++++--- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/shortcuts/slides/slides_screenshot.go b/shortcuts/slides/slides_screenshot.go index ba09549b0..2fb620e5e 100644 --- a/shortcuts/slides/slides_screenshot.go +++ b/shortcuts/slides/slides_screenshot.go @@ -22,7 +22,10 @@ import ( "github.com/larksuite/cli/shortcuts/common" ) -const defaultSlidesScreenshotDir = ".lark-slides/screenshots" +const ( + defaultSlidesScreenshotDir = ".lark-slides/screenshots" + maxSlidesPerScreenshot = 10 +) var unsafeScreenshotFileCharRegex = regexp.MustCompile(`[^A-Za-z0-9._-]+`) @@ -32,7 +35,7 @@ var unsafeScreenshotFileCharRegex = regexp.MustCompile(`[^A-Za-z0-9._-]+`) var SlidesScreenshot = common.Shortcut{ Service: "slides", Command: "+screenshot", - Description: "Save slide screenshots to local files without printing Base64 image data", + Description: "Save up to 10 slide screenshots to local files without printing Base64 image data", Risk: "read", Scopes: []string{}, // The screenshot API is allowlist-gated for only a few apps, so do not @@ -42,8 +45,8 @@ var SlidesScreenshot = common.Shortcut{ 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: "slide-id", Type: "string_array", Desc: "slide page identifier (repeat for multiple slides; max 10 pages per request)"}, + {Name: "slide-number", Type: "int_array", Desc: "slide page number (repeat for multiple slides; max 10 pages per request)"}, {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"}, @@ -70,12 +73,17 @@ var SlidesScreenshot = common.Shortcut{ return err } } - if _, err := normalizeSlideNumbers(runtime.IntArray("slide-number")); err != nil { + slideIDs := normalizeSlideIDs(runtime.StrArray("slide-id")) + slideNumbers, err := normalizeSlideNumbers(runtime.IntArray("slide-number")) + if err != nil { return err } - if !hasSlideScreenshotSelector(runtime) { + if len(slideIDs) == 0 && len(slideNumbers) == 0 { return slidesScreenshotFlagErrorf("--slide-id or --slide-number is required") } + if err := validateSlidesScreenshotSelectorLimit(len(slideIDs) + len(slideNumbers)); err != nil { + return err + } } if _, err := validateScreenshotOutputDir(runtime, runtime.Str("output-dir")); err != nil { return err @@ -98,6 +106,9 @@ var SlidesScreenshot = common.Shortcut{ if len(slideIDs) == 0 && len(slideNumbers) == 0 { return common.NewDryRunAPI().Set("error", "--slide-id or --slide-number is required") } + if err := validateSlidesScreenshotSelectorLimit(len(slideIDs) + len(slideNumbers)); err != nil { + return common.NewDryRunAPI().Set("error", err.Error()) + } presentationID := ref.Token dry := common.NewDryRunAPI() @@ -145,6 +156,9 @@ var SlidesScreenshot = common.Shortcut{ if len(slideIDs) == 0 && len(slideNumbers) == 0 { return slidesScreenshotFlagErrorf("--slide-id or --slide-number is required") } + if err := validateSlidesScreenshotSelectorLimit(len(slideIDs) + len(slideNumbers)); err != nil { + return err + } outputDir := runtime.Str("output-dir") safeOutputDir, err := ensureScreenshotOutputDir(runtime, outputDir) if err != nil { @@ -267,8 +281,11 @@ func normalizeSlideNumbers(values []int) ([]int, error) { return out, nil } -func hasSlideScreenshotSelector(runtime *common.RuntimeContext) bool { - return len(normalizeSlideIDs(runtime.StrArray("slide-id"))) > 0 || len(runtime.IntArray("slide-number")) > 0 +func validateSlidesScreenshotSelectorLimit(count int) error { + if count > maxSlidesPerScreenshot { + return slidesScreenshotFlagErrorf("too many slide selectors: got %d, maximum is %d; request at most 10 pages at a time", count, maxSlidesPerScreenshot) + } + return nil } func slidesScreenshotFlagErrorf(format string, args ...interface{}) error { diff --git a/shortcuts/slides/slides_screenshot_test.go b/shortcuts/slides/slides_screenshot_test.go index 3efae3729..c1a53530f 100644 --- a/shortcuts/slides/slides_screenshot_test.go +++ b/shortcuts/slides/slides_screenshot_test.go @@ -271,6 +271,33 @@ func TestSlidesScreenshotListRequiresSelector(t *testing.T) { } } +func TestSlidesScreenshotListRejectsMoreThanTenSelectors(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, slidesTestConfig(t, "")) + + err := runSlidesShortcut(t, f, stdout, SlidesScreenshot, []string{ + "+screenshot", + "--presentation", "pres_abc", + "--slide-number", "1", + "--slide-number", "2", + "--slide-number", "3", + "--slide-number", "4", + "--slide-number", "5", + "--slide-number", "6", + "--slide-number", "7", + "--slide-number", "8", + "--slide-number", "9", + "--slide-number", "10", + "--slide-number", "11", + "--as", "user", + }) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "request at most 10 pages at a time") { + t.Fatalf("error = %v, want max 10 pages guidance", err) + } +} + func TestSlidesScreenshotRenderContentWritesFile(t *testing.T) { dir := t.TempDir() withSlidesTestWorkingDir(t, dir) diff --git a/skills/lark-slides/SKILL.md b/skills/lark-slides/SKILL.md index cadbe9ed6..1c6311cbf 100644 --- a/skills/lark-slides/SKILL.md +++ b/skills/lark-slides/SKILL.md @@ -32,7 +32,7 @@ metadata: | 从模板创建或编辑已有本地 PPTX | 导入 PPTX 为 Slides | `lark-slides-pptx-template-workflows.md` | | 编辑单个标题、文本块、图片或局部元素 | 优先块级替换/插入,不改页序 | `slides +replace-slide`、`lark-slides-replace-slide.md` | | 读取或分析已有 PPT | 解析 slides/wiki token,用 shortcut 回读全文 XML 或读取单页 XML,保存 `xml_presentation_id`、`slide_id`、`revision_id` | `slides +xml-get`、`xml_presentation.slide.get` | -| 获取幻灯片页面截图 | 用 `slide_id` 或页号指定页面 | `slides +screenshot`、`lark-slides-screenshot.md` | +| 获取幻灯片页面截图 | 用 `slide_id` 或页号指定页面,一次不超过 10 页 | `slides +screenshot`、`lark-slides-screenshot.md` | | 上传或使用图片 | 先上传为 `file_token`,禁止直接写 http(s) 外链 | `slides +media-upload`,或 `+create --slides` 的 `@./path` 占位符 | | 在 slide 中绘制图表 | 雷达图、饼图、环形图用 ``,柱状图、条形图、折线图用 `shape` + `line`,Mermaid 用 `` | `xml-schema-quick-ref.md`、需要 `` 再看 `lark-slides-whiteboard.md` | | 使用图标 | 禁止出现 emoji,必须使用语义图标,禁止盲猜 `iconType`,必须先检索 IconPark,再写 ``,必须设置 fillColor | `iconpark_tool.py search → resolve`、`iconpark.md` | diff --git a/skills/lark-slides/references/lark-slides-screenshot.md b/skills/lark-slides/references/lark-slides-screenshot.md index 23557fcb9..967d71475 100644 --- a/skills/lark-slides/references/lark-slides-screenshot.md +++ b/skills/lark-slides/references/lark-slides-screenshot.md @@ -26,8 +26,8 @@ lark-cli slides +screenshot --as user \ | 参数 | 必需 | 说明 | |------|------|------| | `--presentation` | list 模式必需 | `xml_presentation_id`、`/slides/` URL,或解析后为 slides 的 `/wiki/` URL。传 `--content` 时不能使用 | -| `--slide-id` | list 模式至少提供 `--slide-id` / `--slide-number` 之一 | 页面 short ID;多页截图时重复传入 | -| `--slide-number` | list 模式至少提供 `--slide-id` / `--slide-number` 之一 | 页面页号;多页截图时重复传入 | +| `--slide-id` | list 模式至少提供 `--slide-id` / `--slide-number` 之一 | 页面 short ID;多页截图时重复传入;一次最多 10 页(`--slide-id` + `--slide-number` 合计小于等于 10) | +| `--slide-number` | list 模式至少提供 `--slide-id` / `--slide-number` 之一 | 页面页号;多页截图时重复传入;一次最多 10 页(`--slide-id` + `--slide-number` 合计小于等于 10) | | `--content` | render 模式必需 | 要直接渲染的 `` XML 片段;支持直接传值、`@file`、`-` stdin。传入后不能同时传 `--slide-id` / `--slide-number` | | `--output-dir` | 否 | 输出目录,默认 `.lark-slides/screenshots`;必须是当前目录内的相对路径 | | `--output-name` | 否 | render 模式的输出文件名 stem;未指定时优先用返回的 `slide_id`,否则用 `rendered-slide`。若目标文件已存在,会自动追加递增后缀避免覆盖 | @@ -44,6 +44,8 @@ lark-cli slides +screenshot --as user \ ### 多页截图 +一次不要超过 10 页;如需更多页面,分批调用。 + ```bash lark-cli slides +screenshot --as user \ --presentation slides_example_presentation_id \ @@ -90,5 +92,6 @@ lark-cli slides +screenshot --as user \ 2. 已存在 PPT 页面截图时,不传 `--content`,用 `--presentation` + `--slide-id` 或 `--slide-number`。 3. 本地 XML 预览时,传 `--content @file` 或 `--content -`,内容应为单个 `` XML 片段;此时不要传 `--presentation` / `--slide-id` / `--slide-number`。 4. `slide_id` 是页面 short ID,页码请用 `--slide-number`。 -5. list 模式默认文件名包含 presentation ID、页码和/或 slide ID;文件已存在时自动追加 `_2`、`_3` 等后缀,避免覆盖旧截图。 -6. 截图来自服务端渲染结果,适合创建/替换后验证页面是否为空白、破图或布局明显异常。 +5. list 模式一次最多传 10 页(`--slide-id` + `--slide-number` 合计小于等于 10);更多页面请分批截图。 +6. list 模式默认文件名包含 presentation ID、页码和/或 slide ID;文件已存在时自动追加 `_2`、`_3` 等后缀,避免覆盖旧截图。 +7. 截图来自服务端渲染结果,适合创建/替换后验证页面是否为空白、破图或布局明显异常。