diff --git a/cmd/event/list_test.go b/cmd/event/list_test.go index 1779e064..d47c3db9 100644 --- a/cmd/event/list_test.go +++ b/cmd/event/list_test.go @@ -10,10 +10,22 @@ import ( "github.com/larksuite/cli/internal/cmdutil" "github.com/larksuite/cli/internal/core" + eventlib "github.com/larksuite/cli/internal/event" _ "github.com/larksuite/cli/events" ) +func TestEventLookup_VCMeetingLifecycleKeys(t *testing.T) { + for _, key := range []string{ + "vc.meeting.participant_meeting_started_v1", + "vc.meeting.participant_meeting_joined_v1", + } { + if _, ok := eventlib.Lookup(key); !ok { + t.Fatalf("event.Lookup(%q) should succeed", key) + } + } +} + func TestRunList_TextOutput(t *testing.T) { f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"}) @@ -27,6 +39,8 @@ func TestRunList_TextOutput(t *testing.T) { "im.message.receive_v1", "im.message.message_read_v1", "task.task.update_user_access_v2", + "vc.meeting.participant_meeting_started_v1", + "vc.meeting.participant_meeting_joined_v1", } { if !strings.Contains(out, want) { t.Errorf("list output missing %q; full output:\n%s", want, out) @@ -57,9 +71,15 @@ func TestRunList_JSONOutput(t *testing.T) { } } - var foundTask bool + gotKeys := map[string]map[string]interface{}{} for _, row := range rows { - if row["key"] == "task.task.update_user_access_v2" { + if key, ok := row["key"].(string); ok { + gotKeys[key] = row + } + } + var foundTask bool + for key, row := range gotKeys { + if key == "task.task.update_user_access_v2" { foundTask = true if row["single_consumer"] != true { t.Errorf("task row single_consumer = %v, want true", row["single_consumer"]) @@ -69,4 +89,12 @@ func TestRunList_JSONOutput(t *testing.T) { if !foundTask { t.Fatal("event list JSON missing task.task.update_user_access_v2") } + for _, want := range []string{ + "vc.meeting.participant_meeting_started_v1", + "vc.meeting.participant_meeting_joined_v1", + } { + if _, ok := gotKeys[want]; !ok { + t.Errorf("JSON list output missing %q", want) + } + } } diff --git a/cmd/event/schema_test.go b/cmd/event/schema_test.go index c586dc1a..562fe1b8 100644 --- a/cmd/event/schema_test.go +++ b/cmd/event/schema_test.go @@ -124,6 +124,45 @@ func TestRunSchema_TaskUpdateUserAccessJSON(t *testing.T) { } } +func TestRunSchema_JSONOutput_VCMeetingLifecycleKeys(t *testing.T) { + for _, key := range []string{ + "vc.meeting.participant_meeting_started_v1", + "vc.meeting.participant_meeting_joined_v1", + } { + t.Run(key, func(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"}) + + if err := runSchema(f, key, true); err != nil { + t.Fatalf("runSchema json: %v", err) + } + + var payload map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil { + t.Fatalf("output is not valid JSON: %v\n%s", err, stdout.String()) + } + if payload["key"] != key { + t.Errorf("key = %v, want %s", payload["key"], key) + } + resolved, ok := payload["resolved_output_schema"].(map[string]interface{}) + if !ok { + t.Fatalf("resolved_output_schema missing or wrong type: %+v", payload) + } + properties, ok := resolved["properties"].(map[string]interface{}) + if !ok { + t.Fatalf("resolved_output_schema.properties missing or wrong type: %+v", resolved) + } + for _, field := range []string{"type", "event_id", "timestamp", "meeting_id", "topic", "meeting_no", "start_time", "calendar_event_id"} { + if _, ok := properties[field]; !ok { + t.Errorf("resolved output schema missing field %q: %+v", field, properties) + } + } + if _, ok := properties["end_time"]; ok { + t.Errorf("resolved output schema should not include end_time for %s: %+v", key, properties) + } + }) + } +} + func TestSchema_RendersSubscriptionKeyMarker(t *testing.T) { const syntheticKey = "test.evt_sub" t.Cleanup(func() { eventlib.UnregisterKeyForTest(syntheticKey) }) diff --git a/events/vc/participant_meeting_joined.go b/events/vc/participant_meeting_joined.go new file mode 100644 index 00000000..99ac9e76 --- /dev/null +++ b/events/vc/participant_meeting_joined.go @@ -0,0 +1,62 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package vc + +import ( + "context" + "encoding/json" + + "github.com/larksuite/cli/internal/event" +) + +// VCParticipantMeetingJoinedOutput is the flattened shape for vc.meeting.participant_meeting_joined_v1. +type VCParticipantMeetingJoinedOutput struct { + Type string `json:"type" desc:"Event type; always vc.meeting.participant_meeting_joined_v1"` + EventID string `json:"event_id,omitempty" desc:"Globally unique event ID; safe for deduplication"` + Timestamp string `json:"timestamp,omitempty" desc:"Event delivery time (ms timestamp string); taken from header.create_time when present" kind:"timestamp_ms"` + MeetingID string `json:"meeting_id,omitempty" desc:"Meeting ID" kind:"meeting_id"` + Topic string `json:"topic,omitempty" desc:"Meeting topic"` + MeetingNo string `json:"meeting_no,omitempty" desc:"Meeting number"` + StartTime string `json:"start_time,omitempty" desc:"Meeting start time in RFC3339, converted to the local timezone"` + CalendarEventID string `json:"calendar_event_id,omitempty" desc:"Calendar event ID associated with the meeting"` +} + +func processVCParticipantMeetingJoined(_ context.Context, _ event.APIClient, raw *event.RawEvent, _ map[string]string) (json.RawMessage, error) { + var envelope struct { + Header struct { + EventID string `json:"event_id"` + EventType string `json:"event_type"` + CreateTime string `json:"create_time"` + } `json:"header"` + Event struct { + Meeting struct { + ID string `json:"id"` + Topic string `json:"topic"` + MeetingNo string `json:"meeting_no"` + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` + CalendarEventID string `json:"calendar_event_id"` + } `json:"meeting"` + } `json:"event"` + } + if err := json.Unmarshal(raw.Payload, &envelope); err != nil { + return raw.Payload, nil //nolint:nilerr // passthrough on malformed payload so consumers still see the event + } + + meeting := envelope.Event.Meeting + out := &VCParticipantMeetingJoinedOutput{ + Type: envelope.Header.EventType, + EventID: envelope.Header.EventID, + Timestamp: envelope.Header.CreateTime, + MeetingID: meeting.ID, + Topic: meeting.Topic, + MeetingNo: meeting.MeetingNo, + StartTime: unixSecondsToLocalRFC3339(meeting.StartTime), + CalendarEventID: meeting.CalendarEventID, + } + if out.Type == "" { + out.Type = raw.EventType + } + return json.Marshal(out) +} diff --git a/events/vc/participant_meeting_lifecycle_test.go b/events/vc/participant_meeting_lifecycle_test.go new file mode 100644 index 00000000..c67b9654 --- /dev/null +++ b/events/vc/participant_meeting_lifecycle_test.go @@ -0,0 +1,281 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package vc + +import ( + "context" + "encoding/json" + "reflect" + "testing" + "time" + + "github.com/larksuite/cli/internal/event" +) + +func TestVCKeys_ProcessedMeetingLifecycleRegistered(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + + for _, tc := range []struct { + eventType string + schemaType reflect.Type + }{ + {eventTypeMeetingStarted, reflect.TypeOf(VCParticipantMeetingStartedOutput{})}, + {eventTypeMeetingJoined, reflect.TypeOf(VCParticipantMeetingJoinedOutput{})}, + } { + t.Run(tc.eventType, func(t *testing.T) { + def, ok := event.Lookup(tc.eventType) + if !ok { + t.Fatalf("%s should be registered via Keys()", tc.eventType) + } + if def.Schema.Custom == nil { + t.Error("Processed key must set Schema.Custom") + } + if def.Schema.Native != nil { + t.Error("Processed key must not set Schema.Native") + } + if def.Process == nil { + t.Error("Process must not be nil for processed key") + } + if def.PreConsume == nil { + t.Error("PreConsume must not be nil for processed key") + } + if len(def.Scopes) != 1 || def.Scopes[0] != "vc:meeting.meetingevent:read" { + t.Errorf("Scopes = %v", def.Scopes) + } + if len(def.AuthTypes) != 1 || def.AuthTypes[0] != "user" { + t.Errorf("AuthTypes = %v", def.AuthTypes) + } + if len(def.RequiredConsoleEvents) != 1 || def.RequiredConsoleEvents[0] != tc.eventType { + t.Errorf("RequiredConsoleEvents = %v", def.RequiredConsoleEvents) + } + if def.Schema.Custom.Type != tc.schemaType { + t.Errorf("Custom schema Type = %v, want %v", def.Schema.Custom.Type, tc.schemaType) + } + }) + } +} + +func TestProcessVCParticipantMeetingLifecycle(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + + for _, tc := range []struct { + name string + eventType string + process event.ProcessFunc + }{ + { + name: "started", + eventType: eventTypeMeetingStarted, + process: processVCParticipantMeetingStarted, + }, + { + name: "joined", + eventType: eventTypeMeetingJoined, + process: processVCParticipantMeetingJoined, + }, + } { + t.Run(tc.name, func(t *testing.T) { + payload := `{ + "schema": "2.0", + "header": { + "event_id": "ev_vc_lifecycle_001", + "event_type": "` + tc.eventType + `", + "create_time": "1608725989000", + "app_id": "cli_test" + }, + "event": { + "meeting": { + "id": "6911188411934433028", + "topic": "my meeting", + "meeting_no": "235812466", + "start_time": "1608883322", + "end_time": "1608883899", + "calendar_event_id": "efa67a98-06a8-4df5-8559-746c8f4477ef_0" + } + } + }` + out := runMeetingLifecycleMap(t, tc.eventType, tc.process, payload) + + if out["type"] != tc.eventType { + t.Errorf("type = %q", out["type"]) + } + if out["event_id"] != "ev_vc_lifecycle_001" { + t.Errorf("event_id = %q", out["event_id"]) + } + if out["timestamp"] != "1608725989000" { + t.Errorf("timestamp = %q", out["timestamp"]) + } + if out["meeting_id"] != "6911188411934433028" { + t.Errorf("meeting_id = %q", out["meeting_id"]) + } + if out["topic"] != "my meeting" || out["meeting_no"] != "235812466" { + t.Errorf("topic/meeting_no = %q/%q", out["topic"], out["meeting_no"]) + } + if out["calendar_event_id"] != "efa67a98-06a8-4df5-8559-746c8f4477ef_0" { + t.Errorf("calendar_event_id = %q", out["calendar_event_id"]) + } + if want := time.Unix(1608883322, 0).Local().Format(time.RFC3339); out["start_time"] != want { + t.Errorf("start_time = %q, want %q", out["start_time"], want) + } + if _, hasEndTime := out["end_time"]; hasEndTime { + t.Error("end_time should not be present in started/joined output") + } + }) + } +} + +func TestProcessVCParticipantMeetingLifecycle_InvalidMeetingTimes(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + + for _, tc := range []struct { + name string + eventType string + process event.ProcessFunc + }{ + {"started", eventTypeMeetingStarted, processVCParticipantMeetingStarted}, + {"joined", eventTypeMeetingJoined, processVCParticipantMeetingJoined}, + } { + t.Run(tc.name, func(t *testing.T) { + payload := `{ + "schema": "2.0", + "header": { + "event_id": "ev_vc_lifecycle_002", + "event_type": "` + tc.eventType + `", + "create_time": "1608725989001" + }, + "event": { + "meeting": { + "id": "meeting_invalid_time", + "start_time": "bad", + "end_time": "" + } + } + }` + out := runMeetingLifecycleRaw(t, tc.eventType, tc.process, payload) + switch tc.eventType { + case eventTypeMeetingStarted: + var started VCParticipantMeetingStartedOutput + if err := json.Unmarshal(out, &started); err != nil { + t.Fatalf("Process output is not valid started JSON: %v\nraw=%s", err, string(out)) + } + if started.StartTime != "" { + t.Errorf("StartTime = %q, want empty string", started.StartTime) + } + case eventTypeMeetingJoined: + var joined VCParticipantMeetingJoinedOutput + if err := json.Unmarshal(out, &joined); err != nil { + t.Fatalf("Process output is not valid joined JSON: %v\nraw=%s", err, string(out)) + } + if joined.StartTime != "" { + t.Errorf("StartTime = %q, want empty string", joined.StartTime) + } + } + }) + } +} + +func TestProcessVCParticipantMeetingLifecycle_MalformedPayload(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + + for _, tc := range []struct { + name string + eventType string + process event.ProcessFunc + }{ + {"started", eventTypeMeetingStarted, processVCParticipantMeetingStarted}, + {"joined", eventTypeMeetingJoined, processVCParticipantMeetingJoined}, + } { + t.Run(tc.name, func(t *testing.T) { + raw := &event.RawEvent{ + EventType: tc.eventType, + Payload: json.RawMessage(`not json`), + Timestamp: time.Now(), + } + got, err := tc.process(context.Background(), nil, raw, nil) + if err != nil { + t.Fatalf("Process should swallow parse errors, got %v", err) + } + if string(got) != "not json" { + t.Errorf("malformed fallback output = %q, want original bytes", string(got)) + } + }) + } +} + +func TestVCParticipantMeetingLifecycle_PreConsumeSubscriptionLifecycle(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + + for _, eventType := range []string{eventTypeMeetingStarted, eventTypeMeetingJoined} { + t.Run(eventType, func(t *testing.T) { + def, ok := event.Lookup(eventType) + if !ok { + t.Fatalf("%s should be registered via Keys()", eventType) + } + + type call struct { + method string + path string + body any + } + var calls []call + rt := &stubAPIClient{ + callFn: func(_ context.Context, method, path string, body any) (json.RawMessage, error) { + calls = append(calls, call{method: method, path: path, body: body}) + return json.RawMessage(`{"code":0,"msg":"success","data":{}}`), nil + }, + } + + cleanup, err := def.PreConsume(context.Background(), rt, nil) + if err != nil { + t.Fatalf("PreConsume error: %v", err) + } + if cleanup == nil { + t.Fatal("cleanup must not be nil") + } + if len(calls) != 1 { + t.Fatalf("calls after subscribe = %d, want 1", len(calls)) + } + if calls[0].method != "POST" || calls[0].path != pathMeetingSubscribe { + t.Fatalf("subscribe call = %+v", calls[0]) + } + assertSubscriptionRequest(t, calls[0].body, eventType) + + cleanup() + if len(calls) != 2 { + t.Fatalf("calls after cleanup = %d, want 2", len(calls)) + } + if calls[1].method != "POST" || calls[1].path != pathMeetingUnsubscribe { + t.Fatalf("unsubscribe call = %+v", calls[1]) + } + assertSubscriptionRequest(t, calls[1].body, eventType) + }) + } +} + +func runMeetingLifecycleMap(t *testing.T, eventType string, process event.ProcessFunc, payload string) map[string]string { + t.Helper() + got := runMeetingLifecycleRaw(t, eventType, process, payload) + if got == nil { + t.Fatal("Process output is nil") + } + var out map[string]string + if err := json.Unmarshal(got, &out); err != nil { + t.Fatalf("Process output is not valid flat JSON object: %v\nraw=%s", err, string(got)) + } + return out +} + +func runMeetingLifecycleRaw(t *testing.T, eventType string, process event.ProcessFunc, payload string) json.RawMessage { + t.Helper() + raw := &event.RawEvent{ + EventType: eventType, + Payload: json.RawMessage(payload), + Timestamp: time.Now(), + } + got, err := process(context.Background(), nil, raw, nil) + if err != nil { + t.Fatalf("Process error: %v", err) + } + return got +} diff --git a/events/vc/participant_meeting_started.go b/events/vc/participant_meeting_started.go new file mode 100644 index 00000000..d91aa3eb --- /dev/null +++ b/events/vc/participant_meeting_started.go @@ -0,0 +1,61 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package vc + +import ( + "context" + "encoding/json" + + "github.com/larksuite/cli/internal/event" +) + +// VCParticipantMeetingStartedOutput is the flattened shape for vc.meeting.participant_meeting_started_v1. +type VCParticipantMeetingStartedOutput struct { + Type string `json:"type" desc:"Event type; always vc.meeting.participant_meeting_started_v1"` + EventID string `json:"event_id,omitempty" desc:"Globally unique event ID; safe for deduplication"` + Timestamp string `json:"timestamp,omitempty" desc:"Event delivery time (ms timestamp string); taken from header.create_time when present" kind:"timestamp_ms"` + MeetingID string `json:"meeting_id,omitempty" desc:"Meeting ID" kind:"meeting_id"` + Topic string `json:"topic,omitempty" desc:"Meeting topic"` + MeetingNo string `json:"meeting_no,omitempty" desc:"Meeting number"` + StartTime string `json:"start_time,omitempty" desc:"Meeting start time in RFC3339, converted to the local timezone"` + CalendarEventID string `json:"calendar_event_id,omitempty" desc:"Calendar event ID associated with the meeting"` +} + +func processVCParticipantMeetingStarted(_ context.Context, _ event.APIClient, raw *event.RawEvent, _ map[string]string) (json.RawMessage, error) { + var envelope struct { + Header struct { + EventID string `json:"event_id"` + EventType string `json:"event_type"` + CreateTime string `json:"create_time"` + } `json:"header"` + Event struct { + Meeting struct { + ID string `json:"id"` + Topic string `json:"topic"` + MeetingNo string `json:"meeting_no"` + StartTime string `json:"start_time"` + CalendarEventID string `json:"calendar_event_id"` + } `json:"meeting"` + } `json:"event"` + } + if err := json.Unmarshal(raw.Payload, &envelope); err != nil { + return raw.Payload, nil //nolint:nilerr // passthrough on malformed payload so consumers still see the event + } + + meeting := envelope.Event.Meeting + out := &VCParticipantMeetingStartedOutput{ + Type: envelope.Header.EventType, + EventID: envelope.Header.EventID, + Timestamp: envelope.Header.CreateTime, + MeetingID: meeting.ID, + Topic: meeting.Topic, + MeetingNo: meeting.MeetingNo, + StartTime: unixSecondsToLocalRFC3339(meeting.StartTime), + CalendarEventID: meeting.CalendarEventID, + } + if out.Type == "" { + out.Type = raw.EventType + } + return json.Marshal(out) +} diff --git a/events/vc/register.go b/events/vc/register.go index bfee1241..5b4441ae 100644 --- a/events/vc/register.go +++ b/events/vc/register.go @@ -11,6 +11,8 @@ import ( ) const ( + eventTypeMeetingStarted = "vc.meeting.participant_meeting_started_v1" + eventTypeMeetingJoined = "vc.meeting.participant_meeting_joined_v1" eventTypeMeetingEnded = "vc.meeting.participant_meeting_ended_v1" eventTypeNoteGenerated = "vc.note.generated_v1" eventTypeRecordingStarted = "vc.recording.recording_started_v1" @@ -30,6 +32,38 @@ const ( // Keys returns all VC-domain EventKey definitions. func Keys() []event.KeyDefinition { return []event.KeyDefinition{ + { + Key: eventTypeMeetingStarted, + DisplayName: "Participant meeting started", + Description: "Triggered when a meeting the current user participates in has started", + EventType: eventTypeMeetingStarted, + Schema: event.SchemaDef{ + Custom: &event.SchemaSpec{Type: reflect.TypeOf(VCParticipantMeetingStartedOutput{})}, + }, + Process: processVCParticipantMeetingStarted, + PreConsume: subscriptionPreConsume(eventTypeMeetingStarted, pathMeetingSubscribe, pathMeetingUnsubscribe), + Scopes: []string{"vc:meeting.meetingevent:read"}, + AuthTypes: []string{ + "user", + }, + RequiredConsoleEvents: []string{eventTypeMeetingStarted}, + }, + { + Key: eventTypeMeetingJoined, + DisplayName: "Participant meeting joined", + Description: "Triggered when the current user joins a meeting", + EventType: eventTypeMeetingJoined, + Schema: event.SchemaDef{ + Custom: &event.SchemaSpec{Type: reflect.TypeOf(VCParticipantMeetingJoinedOutput{})}, + }, + Process: processVCParticipantMeetingJoined, + PreConsume: subscriptionPreConsume(eventTypeMeetingJoined, pathMeetingSubscribe, pathMeetingUnsubscribe), + Scopes: []string{"vc:meeting.meetingevent:read"}, + AuthTypes: []string{ + "user", + }, + RequiredConsoleEvents: []string{eventTypeMeetingJoined}, + }, { Key: eventTypeMeetingEnded, DisplayName: "Participant meeting ended", diff --git a/skills/lark-event/SKILL.md b/skills/lark-event/SKILL.md index 217b5055..885446e6 100644 --- a/skills/lark-event/SKILL.md +++ b/skills/lark-event/SKILL.md @@ -1,7 +1,7 @@ --- name: lark-event version: 1.0.0 -description: "Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume ` (covers IM messages/reactions/chat changes, Task updates, VC meeting ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses." +description: "Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume ` (covers IM messages/reactions/chat changes, Task updates, VC meeting started/joined/ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses." metadata: requires: bins: ["lark-cli"] @@ -149,6 +149,6 @@ Lark-defined semantic tags (**not** JSON Schema's standard `format`). Common val |------------|------------------------------------------------------------------------------|---| | IM | [`references/lark-event-im.md`](references/lark-event-im.md) | Catalog of 12 IM EventKeys + shape notes (flat vs V2 envelope) + `im.message.receive_v1` field gotchas (`sender_id` is open_id only; `.content` is plain text except for `interactive` cards) + common jq recipes (filter by chat_type / message_type / sender); for `card.action.trigger` see also [`../lark-im/references/lark-im-card-action-reply.md`](../lark-im/references/lark-im-card-action-reply.md) | | Task | [`references/lark-event-task.md`](references/lark-event-task.md) | Catalog of 1 Task EventKey (`task.task.update_user_access_v2`) + Native V2 envelope shape + task commit types + user/bot subscription notes | -| VC | [`references/lark-event-vc.md`](references/lark-event-vc.md) | Catalog of 2 VC EventKeys (`vc.meeting.participant_meeting_ended_v1`, `vc.note.generated_v1`) + field reference + source type semantics (meeting only) | +| VC | [`references/lark-event-vc.md`](references/lark-event-vc.md) | Catalog of 4 VC EventKeys (`vc.meeting.participant_meeting_started_v1`, `vc.meeting.participant_meeting_joined_v1`, `vc.meeting.participant_meeting_ended_v1`, `vc.note.generated_v1`) + field reference + source type semantics (meeting only) | | Minutes | [`references/lark-event-minutes.md`](references/lark-event-minutes.md) | Catalog of 1 Minutes EventKey (`minutes.minute.generated_v1`) + field reference + source type semantics (meeting only) | | Whiteboard | [`references/lark-event-whiteboard.md`](references/lark-event-whiteboard.md) | Catalog of 1 Board EventKey (`board.whiteboard.updated_v1`) + per-whiteboard subscription model (requires `-p whiteboard_id=`) + payload field reference (whiteboard_id / operator_ids triple-id) | diff --git a/skills/lark-event/references/lark-event-vc.md b/skills/lark-event/references/lark-event-vc.md index 37e54cd5..16679aae 100644 --- a/skills/lark-event/references/lark-event-vc.md +++ b/skills/lark-event/references/lark-event-vc.md @@ -2,48 +2,60 @@ > **Prerequisite:** Read [`../SKILL.md`](../SKILL.md) first for the `event consume` essentials (commands, subprocess contract, jq usage). -## Key catalog (2) +## Key catalog (4) | EventKey | Purpose | |---|---| +| `vc.meeting.participant_meeting_started_v1` | A meeting the current user participates in has started | +| `vc.meeting.participant_meeting_joined_v1` | The current user has joined a meeting | | `vc.meeting.participant_meeting_ended_v1` | A meeting the current user participates in has ended | | `vc.note.generated_v1` | A note has been generated (meeting, recording, upload, etc.) | -Both keys use a **Custom schema** (flat output) and carry a **PreConsume hook** that auto-subscribes / unsubscribes via OAPI on first / last consumer. Both require `--as user`. +All four keys use a **Custom schema** (flat output) and carry a **PreConsume hook** that auto-subscribes / unsubscribes via OAPI on first / last consumer. All require `--as user`. ## Scopes & auth | EventKey | Scope | Auth | |---|---|---| +| `vc.meeting.participant_meeting_started_v1` | `vc:meeting.meetingevent:read` | user | +| `vc.meeting.participant_meeting_joined_v1` | `vc:meeting.meetingevent:read` | user | | `vc.meeting.participant_meeting_ended_v1` | `vc:meeting.meetingevent:read` | user | | `vc.note.generated_v1` | `vc:note:read` | user | --- -## `vc.meeting.participant_meeting_ended_v1` +## Meeting participant events + +Covered keys: + +- `vc.meeting.participant_meeting_started_v1` +- `vc.meeting.participant_meeting_joined_v1` +- `vc.meeting.participant_meeting_ended_v1` ### Output fields | Field | Type | Description | |---|---|---| -| `type` | string | Event type; always `vc.meeting.participant_meeting_ended_v1` | +| `type` | string | Event type; one of the covered meeting participant EventKeys | | `event_id` | string | Globally unique event ID; safe for deduplication | | `timestamp` | string (timestamp_ms) | Event delivery time (ms timestamp string) | | `meeting_id` | string | Meeting ID | | `topic` | string | Meeting topic | | `meeting_no` | string | Meeting number | | `start_time` | string | Meeting start time in RFC3339, converted to the local timezone | -| `end_time` | string | Meeting end time in RFC3339, converted to the local timezone | | `calendar_event_id` | string | Calendar event ID associated with the meeting | +| `end_time` | string | Meeting end time in RFC3339, converted to the local timezone; only present for `vc.meeting.participant_meeting_ended_v1` | ### Gotchas -- `start_time` / `end_time` are **not** the raw unix-seconds from OAPI — the Process hook converts them to local-timezone RFC3339. If the raw value is empty or non-numeric, the field is left empty. +- `start_time` / `end_time` are **not** the raw unix-seconds from OAPI — the Process hook converts them to local-timezone RFC3339. If the raw value is empty or non-numeric, the field is left empty. `end_time` is emitted only for `vc.meeting.participant_meeting_ended_v1`. - No detail API call is made; all fields come from the event payload itself. ### Example ```bash +lark-cli event consume vc.meeting.participant_meeting_started_v1 --as user +lark-cli event consume vc.meeting.participant_meeting_joined_v1 --as user lark-cli event consume vc.meeting.participant_meeting_ended_v1 --as user # Project meeting topic and end time only