diff --git a/shortcuts/apps/apps_file_list.go b/shortcuts/apps/apps_file_list.go index 251d4a2576..36050dc555 100644 --- a/shortcuts/apps/apps_file_list.go +++ b/shortcuts/apps/apps_file_list.go @@ -12,10 +12,23 @@ import ( "github.com/larksuite/cli/shortcuts/common" ) +// maxFileListPageSize 是 file_list 分页上限,与后端 paas_storage checkMaxKeys 的 (0, 200] 契约对齐: +// page_size > 200 服务端直接返回 ErrInvalidRequest("maxKeys not in range (0, 200]")。CLI 前置校验避免无谓往返。 +// 注:服务端对 page_size<=0 会兜底为默认值,但 CLI 默认已是 20、显式传 <1 属误用,故与其它 list 命令一致地按 [1, 200] 校验。 +const maxFileListPageSize = 200 + +// validateFileListPageSize 前置校验 --page-size ∈ [1, maxFileListPageSize],与后端 checkMaxKeys 的 (0, 200] 契约对齐。 +func validateFileListPageSize(n int) error { + if n < 1 || n > maxFileListPageSize { + return appsValidationParamError("--page-size", "--page-size must be between 1 and %d", maxFileListPageSize) + } + return nil +} + // AppsFileList lists files in a Miaoda app's storage (cursor pagination)。 // // GET /apps/{app_id}/storage/file_list。过滤器:--name / --path / --type / --size-gt / -// --size-lt / --uploaded-since / --uploaded-until(精确或区间),分页 --page-size/--page-token。 +// --size-lt / --uploaded-since / --uploaded-until(精确或区间),分页 --page-size(1..200)/--page-token。 // file 域不分 dev/online,无 --env。 // // pretty 渲染 5 列:file_name / path / size / type / uploaded_at;空结果打 "No files found."。 @@ -41,13 +54,17 @@ var AppsFileList = common.Shortcut{ {Name: "size-lt", Type: "int", Desc: "filter: size less than (bytes)"}, {Name: "uploaded-since", Desc: "filter: uploaded at or after; relative (7d/2h/30s) | date (2026-04-15) | datetime (2026-04-15T10:00:00) | ISO 8601 w/ TZ (bare date/datetime read in local timezone)"}, {Name: "uploaded-until", Desc: "filter: uploaded at or before; relative (7d/2h/30s) | date (2026-04-15) | datetime (2026-04-15T10:00:00) | ISO 8601 w/ TZ (bare date/datetime read in local timezone)"}, - {Name: "page-size", Type: "int", Default: "20", Desc: "page size"}, + {Name: "page-size", Type: "int", Default: "20", Desc: "page size (1..200)"}, {Name: "page-token", Desc: "pagination cursor from previous response"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if _, err := requireAppID(rctx.Str("app-id")); err != nil { return err } + // page_size 前置校验:对齐后端 checkMaxKeys 的 (0, 200] 契约,避免 >200 触发服务端 ErrInvalidRequest。 + if err := validateFileListPageSize(rctx.Int("page-size")); err != nil { + return err + } // 设计原则三: 多格式 → 归一化为 RFC3339 UTC,回写到 flag 供 buildFileListParams 透传。 for _, f := range []string{"uploaded-since", "uploaded-until"} { if strings.TrimSpace(rctx.Str(f)) == "" { diff --git a/shortcuts/apps/apps_file_list_test.go b/shortcuts/apps/apps_file_list_test.go index dcfdd3d123..102236d6d7 100644 --- a/shortcuts/apps/apps_file_list_test.go +++ b/shortcuts/apps/apps_file_list_test.go @@ -82,6 +82,34 @@ func TestAppsFileList_RequiresAppID(t *testing.T) { } } +// TestAppsFileList_PageSizeOutOfRange 验证 --page-size 超出 (0, 200] 契约时前置报 --page-size 校验错误,不发请求。 +func TestAppsFileList_PageSizeOutOfRange(t *testing.T) { + for _, ps := range []string{"0", "201", "500"} { + factory, stdout, _ := newAppsExecuteFactory(t) + err := runAppsShortcut(t, AppsFileList, + []string{"+file-list", "--app-id", "app_x", "--page-size", ps, "--as", "user"}, factory, stdout) + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("page-size=%s: err = %T %v, want *errs.ValidationError", ps, err, err) + } + if ve.Param != "--page-size" { + t.Fatalf("page-size=%s: Param = %q, want --page-size", ps, ve.Param) + } + } +} + +// TestAppsFileList_PageSizeBoundaryOK 验证边界值 1 与 200 通过校验(dry-run 不报错并把 page_size 下发)。 +func TestAppsFileList_PageSizeBoundaryOK(t *testing.T) { + for _, ps := range []string{"1", "200"} { + factory, stdout, _ := newAppsExecuteFactory(t) + if err := runAppsShortcut(t, AppsFileList, + []string{"+file-list", "--app-id", "app_x", "--page-size", ps, "--dry-run", "--as", "user"}, + factory, stdout); err != nil { + t.Fatalf("page-size=%s: dry-run err=%v", ps, err) + } + } +} + // 过滤器 + 分页全部进 query(size-gt/lt 走 int,uploaded_since/until 原样)。 func TestAppsFileList_DryRunSendsFiltersAndPagination(t *testing.T) { factory, stdout, _ := newAppsExecuteFactory(t) diff --git a/skills/lark-apps/references/lark-apps-file.md b/skills/lark-apps/references/lark-apps-file.md index 80f46ed78f..50a623fec1 100644 --- a/skills/lark-apps/references/lark-apps-file.md +++ b/skills/lark-apps/references/lark-apps-file.md @@ -28,7 +28,7 @@ ## 各命令 ### +file-list -列出应用文件,支持精确过滤:`--name`(文件名)、`--path`(远端路径)、`--type`(MIME 类型)、`--size-gt`/`--size-lt`(字节)、`--uploaded-since`/`--uploaded-until`(上传时间区间,时间格式见末尾)。分页 `--page-size`(默认 20)/ `--page-token`。列表每项给名称、路径、大小、类型、上传时间(pretty 表格即这 5 列);上传者、下载地址(如有)仅在 JSON 输出里,单文件详情用 `+file-get`。 +列出应用文件,支持精确过滤:`--name`(文件名)、`--path`(远端路径)、`--type`(MIME 类型)、`--size-gt`/`--size-lt`(字节)、`--uploaded-since`/`--uploaded-until`(上传时间区间,时间格式见末尾)。分页 `--page-size`(默认 20,范围 1..200)/ `--page-token`。列表每项给名称、路径、大小、类型、上传时间(pretty 表格即这 5 列);上传者、下载地址(如有)仅在 JSON 输出里,单文件详情用 `+file-get`。 ```bash lark-cli apps +file-list --app-id app_xxx