feat(vc): refine meeting events pagination and output

This commit is contained in:
houzhicong
2026-04-17 15:03:07 +08:00
parent 97ef2bcfb8
commit e9eb91d49c
2 changed files with 245 additions and 22 deletions

View File

@@ -23,8 +23,8 @@ const (
minVCMeetingEventsPageSize = 20
maxVCMeetingEventsPageSize = 100
maxVCMeetingEventSummarySize = 80
defaultVCMeetingPageLimit = 20
maxVCMeetingPageLimit = 40
defaultVCMeetingPageLimit = 50
maxVCMeetingPageLimit = 200
)
// toUnixSeconds converts a supported CLI time input into a Unix seconds string.
@@ -54,8 +54,8 @@ var VCMeetingEvents = common.Shortcut{
{Name: "end", Desc: "time upper bound (ISO 8601, YYYY-MM-DD, or Unix seconds)"},
{Name: "page-token", Desc: "page token for the next page"},
{Name: "page-size", Default: "20", Desc: "page size, 20-100 (default 20)"},
{Name: "page-all", Type: "bool", Desc: "automatically paginate through all pages (max 40)"},
{Name: "page-limit", Type: "int", Default: "20", Desc: "max page limit when --page-all is set (default 20, max 40)"},
{Name: "page-all", Type: "bool", Desc: "automatically paginate through all pages (uses max page limit 200 unless --page-limit is set)"},
{Name: "page-limit", Type: "int", Default: "50", Desc: "max pages to fetch during auto-pagination; setting this flag also enables auto-pagination (default 50, max 200)"},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if err := validateMeetingEventsMeetingID(runtime.Str("meeting-id")); err != nil {
@@ -64,9 +64,6 @@ var VCMeetingEvents = common.Shortcut{
if _, _, err := parseMeetingEventsTimeRange(runtime); err != nil {
return err
}
if _, err := common.ValidatePageSize(runtime, "page-size", defaultVCMeetingEventsSize, minVCMeetingEventsPageSize, maxVCMeetingEventsPageSize); err != nil {
return err
}
if _, err := meetingEventsPageLimit(runtime); err != nil {
return err
}
@@ -81,7 +78,15 @@ var VCMeetingEvents = common.Shortcut{
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
dryRun := common.NewDryRunAPI().GET(vcMeetingEventsAPIPath)
autoPaginate, pageLimit, err := meetingEventsPaginationConfig(runtime)
if err != nil {
return common.NewDryRunAPI().Set("error", err.Error())
}
dryRun := common.NewDryRunAPI()
if autoPaginate {
dryRun = dryRun.Desc(fmt.Sprintf("Auto-paginates up to %d page(s)", pageLimit))
}
dryRun = dryRun.GET(vcMeetingEventsAPIPath)
if flat := flattenQueryParams(params); len(flat) > 0 {
dryRun.Params(flat)
}
@@ -96,6 +101,7 @@ var VCMeetingEvents = common.Shortcut{
if err != nil {
return err
}
events = compactMeetingEvents(events)
rows := buildMeetingEventRows(events)
outData := map[string]interface{}{
"events": events,
@@ -118,6 +124,27 @@ var VCMeetingEvents = common.Shortcut{
},
}
func meetingEventsPageSize(runtime *common.RuntimeContext) (int, error) {
if runtime.Bool("page-all") {
return maxVCMeetingEventsPageSize, nil
}
pageSizeStr := strings.TrimSpace(runtime.Str("page-size"))
if pageSizeStr == "" {
return defaultVCMeetingEventsSize, nil
}
pageSize, err := strconv.Atoi(pageSizeStr)
if err != nil {
return 0, common.FlagErrorf("invalid --page-size %q: must be an integer", pageSizeStr)
}
if pageSize < minVCMeetingEventsPageSize {
return minVCMeetingEventsPageSize, nil
}
if pageSize > maxVCMeetingEventsPageSize {
return maxVCMeetingEventsPageSize, nil
}
return pageSize, nil
}
func meetingEventsPageLimit(runtime *common.RuntimeContext) (int, error) {
limit := runtime.Int("page-limit")
if limit == 0 && !runtime.Cmd.Flags().Changed("page-limit") {
@@ -129,6 +156,21 @@ func meetingEventsPageLimit(runtime *common.RuntimeContext) (int, error) {
return limit, nil
}
func meetingEventsPaginationConfig(runtime *common.RuntimeContext) (autoPaginate bool, pageLimit int, err error) {
pageLimit, err = meetingEventsPageLimit(runtime)
if err != nil {
return false, 0, err
}
autoPaginate = runtime.Bool("page-all")
if runtime.Cmd != nil && runtime.Cmd.Flags().Changed("page-limit") {
autoPaginate = true
}
if runtime.Bool("page-all") && (runtime.Cmd == nil || !runtime.Cmd.Flags().Changed("page-limit")) {
pageLimit = maxVCMeetingPageLimit
}
return autoPaginate, pageLimit, nil
}
func validateMeetingEventsMeetingID(meetingID string) error {
meetingID = strings.TrimSpace(meetingID)
if meetingID == "" {
@@ -175,7 +217,7 @@ func parseMeetingEventsTimeRange(runtime *common.RuntimeContext) (string, string
}
func buildMeetingEventsParams(runtime *common.RuntimeContext, startTime, endTime string) (larkcore.QueryParams, error) {
pageSize, err := common.ValidatePageSize(runtime, "page-size", defaultVCMeetingEventsSize, minVCMeetingEventsPageSize, maxVCMeetingEventsPageSize)
pageSize, err := meetingEventsPageSize(runtime)
if err != nil {
return nil, err
}
@@ -200,7 +242,11 @@ func fetchMeetingEvents(ctx context.Context, runtime *common.RuntimeContext, sta
if err != nil {
return nil, nil, false, "", err
}
if !runtime.Bool("page-all") {
autoPaginate, pageLimit, err := meetingEventsPaginationConfig(runtime)
if err != nil {
return nil, nil, false, "", err
}
if !autoPaginate {
data, err := runtime.DoAPIJSON(http.MethodGet, vcMeetingEventsAPIPath, params, nil)
if err != nil {
return nil, nil, false, "", err
@@ -214,10 +260,6 @@ func fetchMeetingEvents(ctx context.Context, runtime *common.RuntimeContext, sta
return data, events, hasMore, pageToken, nil
}
pageLimit, err := meetingEventsPageLimit(runtime)
if err != nil {
return nil, nil, false, "", err
}
var (
allEvents []interface{}
lastData map[string]interface{}
@@ -272,6 +314,35 @@ func flattenQueryParams(params larkcore.QueryParams) map[string]interface{} {
return flat
}
func compactMeetingEvents(events []interface{}) []interface{} {
compacted := make([]interface{}, 0, len(events))
for _, raw := range events {
event, _ := raw.(map[string]interface{})
if event == nil {
continue
}
if payload := common.GetMap(event, "payload"); payload != nil {
event["payload"] = compactMeetingPayload(payload)
}
compacted = append(compacted, event)
}
return compacted
}
func compactMeetingPayload(payload map[string]interface{}) map[string]interface{} {
if payload == nil {
return nil
}
compacted := make(map[string]interface{}, len(payload))
for key, value := range payload {
if items, ok := value.([]interface{}); ok && len(items) == 0 {
continue
}
compacted[key] = value
}
return compacted
}
func buildMeetingEventRows(events []interface{}) []map[string]interface{} {
rows := make([]map[string]interface{}, 0, len(events))
for _, raw := range events {

View File

@@ -23,7 +23,7 @@ func newMeetingEventsRuntime() *common.RuntimeContext {
cmd.Flags().String("page-token", "", "")
cmd.Flags().String("page-size", "", "")
cmd.Flags().Bool("page-all", false, "")
cmd.Flags().Int("page-limit", 20, "")
cmd.Flags().Int("page-limit", 50, "")
return common.TestNewRuntimeContext(cmd, defaultConfig())
}
@@ -64,6 +64,32 @@ func participantJoinedEvent() map[string]interface{} {
}
}
func chatReceivedEvent() map[string]interface{} {
return map[string]interface{}{
"event_id": "event-2",
"event_type": "chat_received",
"event_time": "2026-04-17T08:05:00Z",
"payload": map[string]interface{}{
"activity_event_type": "chat_received",
"participant_joined_items": []interface{}{},
"participant_left_items": []interface{}{},
"transcript_received_items": []interface{}{},
"magic_share_started_items": []interface{}{},
"magic_share_ended_items": []interface{}{},
"chat_received_items": []interface{}{
map[string]interface{}{
"content": "hello",
"message_type": 3,
"operator": map[string]interface{}{
"id": "u1",
"user_name": "Alice",
},
},
},
},
}
}
func TestChatReceivedSummary_MultipleItems(t *testing.T) {
payload := map[string]interface{}{
"chat_received_items": []interface{}{
@@ -149,30 +175,39 @@ func TestMeetingEvents_Validation_InvalidTimeRange(t *testing.T) {
}
}
func TestMeetingEvents_Validation_InvalidPageSize(t *testing.T) {
func TestMeetingEvents_Validation_PageSizeBelowMinDoesNotError(t *testing.T) {
runtime := newMeetingEventsRuntime()
_ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381")
_ = runtime.Cmd.Flags().Set("page-size", "10")
err := VCMeetingEvents.Validate(context.Background(), runtime)
if err == nil {
t.Fatal("expected validation error for invalid page size")
if err != nil {
t.Fatalf("expected no validation error for page-size clamp, got: %v", err)
}
if !strings.Contains(err.Error(), "must be between 20 and 100") {
t.Fatalf("unexpected error: %v", err)
}
func TestMeetingEvents_Validation_PageAllIgnoresInvalidPageSize(t *testing.T) {
runtime := newMeetingEventsRuntime()
_ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381")
_ = runtime.Cmd.Flags().Set("page-all", "true")
_ = runtime.Cmd.Flags().Set("page-size", "10")
err := VCMeetingEvents.Validate(context.Background(), runtime)
if err != nil {
t.Fatalf("expected no validation error when page-all ignores page-size, got: %v", err)
}
}
func TestMeetingEvents_Validation_InvalidPageLimit(t *testing.T) {
runtime := newMeetingEventsRuntime()
_ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381")
_ = runtime.Cmd.Flags().Set("page-limit", "41")
_ = runtime.Cmd.Flags().Set("page-limit", "201")
err := VCMeetingEvents.Validate(context.Background(), runtime)
if err == nil {
t.Fatal("expected validation error for invalid page limit")
}
if !strings.Contains(err.Error(), "must be between 1 and 40") {
if !strings.Contains(err.Error(), "must be between 1 and 200") {
t.Fatalf("unexpected error: %v", err)
}
}
@@ -204,6 +239,49 @@ func TestBuildMeetingEventsParams(t *testing.T) {
}
}
func TestBuildMeetingEventsParams_PageSizeBelowMinClampsToMin(t *testing.T) {
runtime := newMeetingEventsRuntime()
_ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381")
_ = runtime.Cmd.Flags().Set("page-size", "10")
params, err := buildMeetingEventsParams(runtime, "", "")
if err != nil {
t.Fatalf("buildMeetingEventsParams() error = %v", err)
}
if got := params["page_size"][0]; got != "20" {
t.Fatalf("page_size = %q, want %q when below min", got, "20")
}
}
func TestBuildMeetingEventsParams_PageSizeAboveMaxClampsToMax(t *testing.T) {
runtime := newMeetingEventsRuntime()
_ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381")
_ = runtime.Cmd.Flags().Set("page-size", "999")
params, err := buildMeetingEventsParams(runtime, "", "")
if err != nil {
t.Fatalf("buildMeetingEventsParams() error = %v", err)
}
if got := params["page_size"][0]; got != "100" {
t.Fatalf("page_size = %q, want %q when above max", got, "100")
}
}
func TestBuildMeetingEventsParams_PageAllUsesMaxPageSize(t *testing.T) {
runtime := newMeetingEventsRuntime()
_ = runtime.Cmd.Flags().Set("meeting-id", "7628568141510692381")
_ = runtime.Cmd.Flags().Set("page-all", "true")
_ = runtime.Cmd.Flags().Set("page-size", "50")
params, err := buildMeetingEventsParams(runtime, "", "")
if err != nil {
t.Fatalf("buildMeetingEventsParams() error = %v", err)
}
if got := params["page_size"][0]; got != "100" {
t.Fatalf("page_size = %q, want %q when page-all is set", got, "100")
}
}
func TestMeetingEvents_DryRun(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
err := mountAndRun(t, VCMeetingEvents, []string{
@@ -235,6 +313,24 @@ func TestMeetingEvents_DryRun(t *testing.T) {
}
}
func TestMeetingEvents_DryRun_PageAllUsesMaxLimit(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
"--meeting-id", "7628568141510692381",
"--page-all",
"--dry-run",
"--as", "user",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(stdout.String(), "Auto-paginates up to 200 page(s)") {
t.Fatalf("dry-run output missing auto-pagination description: %s", stdout.String())
}
}
func TestMeetingEvents_ExecuteJSON_PageAll(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "pt_2"))
@@ -263,6 +359,30 @@ func TestMeetingEvents_ExecuteJSON_PageAll(t *testing.T) {
}
}
func TestMeetingEvents_ExecuteJSON_PageLimitEnablesPagination(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "pt_2"))
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, false, ""))
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
"--meeting-id", "7628568141510692381",
"--format", "json",
"--page-limit", "2",
"--as", "user",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
reg.Verify(t)
out := strings.ReplaceAll(stdout.String(), " ", "")
out = strings.ReplaceAll(out, "\n", "")
if count := strings.Count(out, `"event_type":"participant_joined"`); count != 2 {
t.Fatalf("expected 2 aggregated events, got %d: %s", count, stdout.String())
}
}
func TestMeetingEvents_ExecuteJSON(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "1710000000000000000"))
@@ -292,6 +412,38 @@ func TestMeetingEvents_ExecuteJSON(t *testing.T) {
}
}
func TestMeetingEvents_ExecuteJSON_PrunesEmptySlices(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{chatReceivedEvent()}, false, ""))
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
"--meeting-id", "7628568141510692381",
"--format", "json",
"--as", "user",
}, f, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
reg.Verify(t)
out := stdout.String()
for _, unwanted := range []string{
`"participant_joined_items": []`,
`"participant_left_items": []`,
`"transcript_received_items": []`,
`"magic_share_started_items": []`,
`"magic_share_ended_items": []`,
} {
if strings.Contains(out, unwanted) {
t.Fatalf("json output should not contain %q: %s", unwanted, out)
}
}
if !strings.Contains(out, `"message_type": 3`) {
t.Fatalf("json output should keep numeric fields: %s", out)
}
}
func TestMeetingEvents_ExecutePretty(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "1710000000000000000"))