mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 10:08:02 +08:00
fix: limit slides screenshot page requests
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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. 截图来自服务端渲染结果,适合创建/替换后验证页面是否为空白、破图或布局明显异常。
|
||||
|
||||
Reference in New Issue
Block a user