fix: limit slides screenshot page requests

This commit is contained in:
zhanghuanxu
2026-07-08 02:16:18 +08:00
parent d719d131f6
commit da7e954dc9
4 changed files with 60 additions and 13 deletions

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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 中绘制图表 | 雷达图、饼图、环形图用 `<chart>`,柱状图、条形图、折线图用 `shape` + `line`Mermaid 用 `<whiteboard>` | `xml-schema-quick-ref.md`、需要 `<whiteboard>` 再看 `lark-slides-whiteboard.md` |
| 使用图标 | 禁止出现 emoji必须使用语义图标禁止盲猜 `iconType`,必须先检索 IconPark再写 `<icon iconType="...">`,必须设置 fillColor | `iconpark_tool.py search → resolve``iconpark.md` |

View File

@@ -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 模式必需 | 要直接渲染的 `<slide>` 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 -`,内容应为单个 `<slide>` 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. 截图来自服务端渲染结果,适合创建/替换后验证页面是否为空白、破图或布局明显异常。