mirror of
https://github.com/larksuite/cli.git
synced 2026-07-04 06:29:52 +08:00
Compare commits
1 Commits
feat/sheet
...
features/F
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8581b1cd4c |
174
events/vc/bot_events.go
Normal file
174
events/vc/bot_events.go
Normal file
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package vc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
|
||||
"github.com/larksuite/cli/internal/event"
|
||||
)
|
||||
|
||||
// VCBotEventOutput is the raw-preserving shape for bot-observed VC events.
|
||||
type VCBotEventOutput struct {
|
||||
Type string `json:"type" desc:"Event type; one of the supported vc.bot.* keys"`
|
||||
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"`
|
||||
CallID string `json:"call_id,omitempty" desc:"Bot invitation call ID; pass through to vc agent join when present"`
|
||||
MeetingNo string `json:"meeting_no,omitempty" desc:"Meeting number when present in the bot event payload"`
|
||||
ActivityEventType string `json:"activity_event_type,omitempty" desc:"Meeting activity event subtype when present"`
|
||||
ChatEmojiTypes []string `json:"chat_emoji_types,omitempty" desc:"Feishu post emotion emoji_type values extracted from vc.bot.meeting_event_v1 payloads"`
|
||||
RawEvent json.RawMessage `json:"raw_event,omitempty" desc:"Original VC bot event payload; authoritative for fields not normalized by lark-cli"`
|
||||
}
|
||||
|
||||
func processVCBotMeetingInvited(_ context.Context, _ event.APIClient, raw *event.RawEvent, _ map[string]string) (json.RawMessage, error) {
|
||||
return processVCBotEvent(raw, false)
|
||||
}
|
||||
|
||||
func processVCBotMeetingEvent(_ context.Context, _ event.APIClient, raw *event.RawEvent, _ map[string]string) (json.RawMessage, error) {
|
||||
return processVCBotEvent(raw, true)
|
||||
}
|
||||
|
||||
func processVCBotMeetingEnded(_ context.Context, _ event.APIClient, raw *event.RawEvent, _ map[string]string) (json.RawMessage, error) {
|
||||
return processVCBotEvent(raw, false)
|
||||
}
|
||||
|
||||
func processVCBotEvent(raw *event.RawEvent, includeEmojiTypes bool) (json.RawMessage, error) {
|
||||
var payload any
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw.Payload))
|
||||
decoder.UseNumber()
|
||||
if err := decoder.Decode(&payload); err != nil {
|
||||
return raw.Payload, nil //nolint:nilerr // passthrough on malformed payload so consumers still see the event
|
||||
}
|
||||
|
||||
out := &VCBotEventOutput{
|
||||
Type: firstString(payload, "event_type"),
|
||||
EventID: firstString(payload, "event_id"),
|
||||
Timestamp: firstString(payload, "create_time"),
|
||||
CallID: firstString(payload, "call_id"),
|
||||
MeetingNo: firstString(payload, "meeting_no"),
|
||||
ActivityEventType: firstString(payload, "activity_event_type"),
|
||||
RawEvent: append(json.RawMessage(nil), raw.Payload...),
|
||||
}
|
||||
if out.Type == "" {
|
||||
out.Type = raw.EventType
|
||||
}
|
||||
if includeEmojiTypes {
|
||||
out.ChatEmojiTypes = botEmojiTypes(payload)
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
func firstString(value any, key string) string {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
if raw, ok := v[key]; ok {
|
||||
if s := jsonString(raw); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
for _, child := range orderedChildren(v) {
|
||||
if s := firstString(child, key); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, child := range v {
|
||||
if s := firstString(child, key); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func orderedChildren(v map[string]any) []any {
|
||||
priority := []string{"header", "event", "meeting", "meeting_info", "activity", "message", "reaction_type"}
|
||||
out := make([]any, 0, len(v))
|
||||
used := make(map[string]bool, len(priority))
|
||||
for _, key := range priority {
|
||||
if child, ok := v[key]; ok {
|
||||
out = append(out, child)
|
||||
used[key] = true
|
||||
}
|
||||
}
|
||||
rest := make([]string, 0, len(v))
|
||||
for key := range v {
|
||||
if !used[key] {
|
||||
rest = append(rest, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(rest)
|
||||
for _, key := range rest {
|
||||
out = append(out, v[key])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func botEmojiTypes(value any) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
collectEmojiTypes(value, seen, &out)
|
||||
return out
|
||||
}
|
||||
|
||||
func collectEmojiTypes(value any, seen map[string]bool, out *[]string) {
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
for _, key := range []string{"emoji_type", "chat_emoji_type"} {
|
||||
if s := jsonString(v[key]); s != "" && !seen[s] {
|
||||
seen[s] = true
|
||||
*out = append(*out, s)
|
||||
}
|
||||
}
|
||||
if raw, ok := v["chat_emoji_types"]; ok {
|
||||
for _, s := range jsonStringSlice(raw) {
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
*out = append(*out, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, child := range v {
|
||||
collectEmojiTypes(child, seen, out)
|
||||
}
|
||||
case []any:
|
||||
for _, child := range v {
|
||||
collectEmojiTypes(child, seen, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func jsonString(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return v
|
||||
case json.Number:
|
||||
return v.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func jsonStringSlice(value any) []string {
|
||||
switch v := value.(type) {
|
||||
case []any:
|
||||
out := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if s := jsonString(item); s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
return append([]string(nil), v...)
|
||||
case string:
|
||||
if v == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{v}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
206
events/vc/bot_events_test.go
Normal file
206
events/vc/bot_events_test.go
Normal file
@@ -0,0 +1,206 @@
|
||||
// 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_BotEventsRegistered(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
for _, eventType := range []string{
|
||||
eventTypeBotMeetingInvited,
|
||||
eventTypeBotMeetingEvent,
|
||||
eventTypeBotMeetingEnded,
|
||||
} {
|
||||
t.Run(eventType, func(t *testing.T) {
|
||||
def, ok := event.Lookup(eventType)
|
||||
if !ok {
|
||||
t.Fatalf("%s should be registered via Keys()", eventType)
|
||||
}
|
||||
if def.Schema.Custom == nil {
|
||||
t.Error("bot event must set Schema.Custom")
|
||||
}
|
||||
if def.Schema.Native != nil {
|
||||
t.Error("bot event must not set Schema.Native")
|
||||
}
|
||||
if def.Process == nil {
|
||||
t.Error("bot event Process must not be nil")
|
||||
}
|
||||
if def.PreConsume != nil {
|
||||
t.Fatal("bot event must not reuse user-side VC PreConsume subscription")
|
||||
}
|
||||
if !reflect.DeepEqual(def.AuthTypes, []string{"bot"}) {
|
||||
t.Errorf("AuthTypes = %v, want [bot]", def.AuthTypes)
|
||||
}
|
||||
if !reflect.DeepEqual(def.RequiredConsoleEvents, []string{eventType}) {
|
||||
t.Errorf("RequiredConsoleEvents = %v, want [%s]", def.RequiredConsoleEvents, eventType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessVCBotEvents_StableFieldsAndRawEvent(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
eventType string
|
||||
process event.ProcessFunc
|
||||
payload string
|
||||
want VCBotEventOutput
|
||||
wantEmojis []string
|
||||
}{
|
||||
{
|
||||
name: "invited",
|
||||
eventType: eventTypeBotMeetingInvited,
|
||||
process: processVCBotMeetingInvited,
|
||||
payload: `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_invited",
|
||||
"event_type": "vc.bot.meeting_invited_v1",
|
||||
"create_time": "1776409469273"
|
||||
},
|
||||
"event": {
|
||||
"call_id": "call_123",
|
||||
"meeting": {"meeting_no": "123456789"}
|
||||
}
|
||||
}`,
|
||||
want: VCBotEventOutput{
|
||||
Type: eventTypeBotMeetingInvited,
|
||||
EventID: "ev_invited",
|
||||
Timestamp: "1776409469273",
|
||||
CallID: "call_123",
|
||||
MeetingNo: "123456789",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "meeting event",
|
||||
eventType: eventTypeBotMeetingEvent,
|
||||
process: processVCBotMeetingEvent,
|
||||
payload: `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_activity",
|
||||
"event_type": "vc.bot.meeting_event_v1",
|
||||
"create_time": "1776409469274"
|
||||
},
|
||||
"event": {
|
||||
"meeting_no": "987654321",
|
||||
"activity_event_type": "chat_message",
|
||||
"chat_messages": [
|
||||
{"message_type": 3, "reaction_type": {"emoji_type": "JIAYI"}},
|
||||
{"message_type": 3, "chat_emoji_types": ["OK", "JIAYI"]}
|
||||
]
|
||||
}
|
||||
}`,
|
||||
want: VCBotEventOutput{
|
||||
Type: eventTypeBotMeetingEvent,
|
||||
EventID: "ev_activity",
|
||||
Timestamp: "1776409469274",
|
||||
MeetingNo: "987654321",
|
||||
ActivityEventType: "chat_message",
|
||||
},
|
||||
wantEmojis: []string{"JIAYI", "OK"},
|
||||
},
|
||||
{
|
||||
name: "ended",
|
||||
eventType: eventTypeBotMeetingEnded,
|
||||
process: processVCBotMeetingEnded,
|
||||
payload: `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_ended",
|
||||
"event_type": "vc.bot.meeting_ended_v1",
|
||||
"create_time": "1776409469275"
|
||||
},
|
||||
"event": {
|
||||
"meeting_no": "246801357"
|
||||
}
|
||||
}`,
|
||||
want: VCBotEventOutput{
|
||||
Type: eventTypeBotMeetingEnded,
|
||||
EventID: "ev_ended",
|
||||
Timestamp: "1776409469275",
|
||||
MeetingNo: "246801357",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out := runBotEventProcess(t, tc.eventType, tc.process, tc.payload)
|
||||
if out.Type != tc.want.Type || out.EventID != tc.want.EventID || out.Timestamp != tc.want.Timestamp {
|
||||
t.Errorf("type/event_id/timestamp = %q/%q/%q", out.Type, out.EventID, out.Timestamp)
|
||||
}
|
||||
if out.CallID != tc.want.CallID {
|
||||
t.Errorf("CallID = %q, want %q", out.CallID, tc.want.CallID)
|
||||
}
|
||||
if out.MeetingNo != tc.want.MeetingNo {
|
||||
t.Errorf("MeetingNo = %q, want %q", out.MeetingNo, tc.want.MeetingNo)
|
||||
}
|
||||
if out.ActivityEventType != tc.want.ActivityEventType {
|
||||
t.Errorf("ActivityEventType = %q, want %q", out.ActivityEventType, tc.want.ActivityEventType)
|
||||
}
|
||||
if !reflect.DeepEqual(out.ChatEmojiTypes, tc.wantEmojis) {
|
||||
t.Errorf("ChatEmojiTypes = %v, want %v", out.ChatEmojiTypes, tc.wantEmojis)
|
||||
}
|
||||
if len(out.RawEvent) == 0 {
|
||||
t.Fatal("RawEvent must be preserved")
|
||||
}
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(out.RawEvent, &raw); err != nil {
|
||||
t.Fatalf("RawEvent is not valid JSON: %v", err)
|
||||
}
|
||||
if raw["schema"] != "2.0" {
|
||||
t.Errorf("RawEvent schema = %v, want 2.0", raw["schema"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessVCBotMeetingEvent_MalformedPassthrough(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
raw := &event.RawEvent{
|
||||
EventID: "ev_bad",
|
||||
EventType: eventTypeBotMeetingEvent,
|
||||
Payload: json.RawMessage(`not json`),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
got, err := processVCBotMeetingEvent(context.Background(), nil, raw, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("process error: %v", err)
|
||||
}
|
||||
if string(got) != "not json" {
|
||||
t.Fatalf("malformed payload passthrough = %s, want raw payload", string(got))
|
||||
}
|
||||
}
|
||||
|
||||
func runBotEventProcess(t *testing.T, eventType string, process event.ProcessFunc, payload string) VCBotEventOutput {
|
||||
t.Helper()
|
||||
raw := &event.RawEvent{
|
||||
EventID: "raw_" + eventType,
|
||||
EventType: eventType,
|
||||
Payload: json.RawMessage(payload),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
got, err := process(context.Background(), nil, raw, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("process %s: %v", eventType, err)
|
||||
}
|
||||
var out VCBotEventOutput
|
||||
if err := json.Unmarshal(got, &out); err != nil {
|
||||
t.Fatalf("unmarshal output: %v\n%s", err, string(got))
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -16,6 +16,9 @@ const (
|
||||
eventTypeRecordingStarted = "vc.recording.recording_started_v1"
|
||||
eventTypeRecordingTranscriptGenerated = "vc.recording.recording_transcript_generated_v1"
|
||||
eventTypeRecordingEnded = "vc.recording.recording_ended_v1"
|
||||
eventTypeBotMeetingInvited = "vc.bot.meeting_invited_v1"
|
||||
eventTypeBotMeetingEvent = "vc.bot.meeting_event_v1"
|
||||
eventTypeBotMeetingEnded = "vc.bot.meeting_ended_v1"
|
||||
|
||||
pathMeetingSubscribe = "/open-apis/vc/v1/meetings/subscription"
|
||||
pathMeetingUnsubscribe = "/open-apis/vc/v1/meetings/unsubscription"
|
||||
@@ -110,5 +113,41 @@ func Keys() []event.KeyDefinition {
|
||||
},
|
||||
RequiredConsoleEvents: []string{eventTypeRecordingEnded},
|
||||
},
|
||||
{
|
||||
Key: eventTypeBotMeetingInvited,
|
||||
DisplayName: "Bot meeting invited",
|
||||
Description: "Triggered when the bot is invited to a meeting; bot-observed event that does not create a user-side VC subscription",
|
||||
EventType: eventTypeBotMeetingInvited,
|
||||
Schema: event.SchemaDef{
|
||||
Custom: &event.SchemaSpec{Type: reflect.TypeOf(VCBotEventOutput{})},
|
||||
},
|
||||
Process: processVCBotMeetingInvited,
|
||||
AuthTypes: []string{"bot"},
|
||||
RequiredConsoleEvents: []string{eventTypeBotMeetingInvited},
|
||||
},
|
||||
{
|
||||
Key: eventTypeBotMeetingEvent,
|
||||
DisplayName: "Bot meeting event",
|
||||
Description: "Triggered when the bot observes activity in a meeting; keeps the raw bot payload and extracts stable activity fields",
|
||||
EventType: eventTypeBotMeetingEvent,
|
||||
Schema: event.SchemaDef{
|
||||
Custom: &event.SchemaSpec{Type: reflect.TypeOf(VCBotEventOutput{})},
|
||||
},
|
||||
Process: processVCBotMeetingEvent,
|
||||
AuthTypes: []string{"bot"},
|
||||
RequiredConsoleEvents: []string{eventTypeBotMeetingEvent},
|
||||
},
|
||||
{
|
||||
Key: eventTypeBotMeetingEnded,
|
||||
DisplayName: "Bot meeting ended",
|
||||
Description: "Triggered when a meeting observed by the bot has ended; distinct from user participant or open meeting resource events",
|
||||
EventType: eventTypeBotMeetingEnded,
|
||||
Schema: event.SchemaDef{
|
||||
Custom: &event.SchemaSpec{Type: reflect.TypeOf(VCBotEventOutput{})},
|
||||
},
|
||||
Process: processVCBotMeetingEnded,
|
||||
AuthTypes: []string{"bot"},
|
||||
RequiredConsoleEvents: []string{eventTypeBotMeetingEnded},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) | VC user events plus bot-observed EventKeys (`vc.bot.meeting_invited_v1`, `vc.bot.meeting_event_v1`, `vc.bot.meeting_ended_v1`) + raw_event/stable field reference + post emotion guidance |
|
||||
| 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=<token>`) + payload field reference (whiteboard_id / operator_ids triple-id) |
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
> **Prerequisite:** Read [`../SKILL.md`](../SKILL.md) first for the `event consume` essentials (commands, subprocess contract, jq usage).
|
||||
|
||||
## Key catalog (2)
|
||||
## Key catalog
|
||||
|
||||
| EventKey | Purpose |
|
||||
|---|---|
|
||||
| `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.) |
|
||||
| `vc.bot.meeting_invited_v1` | The bot is invited to a meeting |
|
||||
| `vc.bot.meeting_event_v1` | The bot observes meeting activity |
|
||||
| `vc.bot.meeting_ended_v1` | A meeting observed by the bot has ended |
|
||||
|
||||
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`.
|
||||
The user VC keys use a **Custom schema** (flat output) and carry a **PreConsume hook** that auto-subscribes / unsubscribes via OAPI on first / last consumer. They require `--as user`.
|
||||
|
||||
The `vc.bot.*` keys are bot-observed events. They require `--as bot`, keep the original payload in `raw_event`, and do not call the user-side VC meeting subscription / unsubscription APIs.
|
||||
|
||||
## Scopes & auth
|
||||
|
||||
@@ -17,6 +22,9 @@ Both keys use a **Custom schema** (flat output) and carry a **PreConsume hook**
|
||||
|---|---|---|
|
||||
| `vc.meeting.participant_meeting_ended_v1` | `vc:meeting.meetingevent:read` | user |
|
||||
| `vc.note.generated_v1` | `vc:note:read` | user |
|
||||
| `vc.bot.meeting_invited_v1` | App event subscription in the Developer Console | bot |
|
||||
| `vc.bot.meeting_event_v1` | App event subscription in the Developer Console | bot |
|
||||
| `vc.bot.meeting_ended_v1` | App event subscription in the Developer Console | bot |
|
||||
|
||||
---
|
||||
|
||||
@@ -92,3 +100,60 @@ lark-cli event consume vc.note.generated_v1 --as user \
|
||||
lark-cli event consume vc.note.generated_v1 --as user \
|
||||
--jq 'select(.note_source.source_type == "meeting") | {note_id, meeting_id: .note_source.source_entity_id}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bot-observed VC events
|
||||
|
||||
Use bot identity for all `vc.bot.*` keys:
|
||||
|
||||
```bash
|
||||
lark-cli event consume vc.bot.meeting_invited_v1 --as bot
|
||||
lark-cli event consume vc.bot.meeting_event_v1 --as bot
|
||||
lark-cli event consume vc.bot.meeting_ended_v1 --as bot
|
||||
```
|
||||
|
||||
These keys model what the bot observes. Do not treat them as aliases for:
|
||||
|
||||
| Bot event | Not the same as |
|
||||
|---|---|
|
||||
| `vc.bot.meeting_invited_v1` | Meeting start events, participant join events, or IM meeting cards |
|
||||
| `vc.bot.meeting_event_v1` | User-side `vc +meeting-events` open meeting activity queries |
|
||||
| `vc.bot.meeting_ended_v1` | `vc.meeting.participant_meeting_ended_v1` or open meeting resource ended events |
|
||||
|
||||
### Output fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `type` | string | Event type; one of the supported `vc.bot.*` keys |
|
||||
| `event_id` | string | Globally unique event ID; safe for deduplication |
|
||||
| `timestamp` | string (timestamp_ms) | Event delivery time from `header.create_time` when present |
|
||||
| `call_id` | string | Invitation call ID; pass through to VC agent join when present |
|
||||
| `meeting_no` | string | Meeting number when present in the payload |
|
||||
| `activity_event_type` | string | Meeting activity subtype when present |
|
||||
| `chat_emoji_types` | string[] | Feishu post emotion `emoji_type` values extracted from `vc.bot.meeting_event_v1` payloads |
|
||||
| `raw_event` | object | Original bot event payload; authoritative for fields not normalized by `lark-cli` |
|
||||
|
||||
Malformed or evolving payloads are not over-normalized. If a payload cannot be parsed, `event consume` passes the raw payload through; if a field is not recognized, read `raw_event`.
|
||||
|
||||
### Post emotion forwarding
|
||||
|
||||
`lark-cli event consume` does not send IM messages automatically. When `vc.bot.meeting_event_v1` exposes `chat_emoji_types`, an agent can explicitly forward the emotion by sending a Feishu `post` message whose content uses `tag: "emotion"` and `emoji_type` from `chat_emoji_types`.
|
||||
|
||||
```bash
|
||||
lark-cli im +messages-send \
|
||||
--chat-id <chat_id> \
|
||||
--msg-type post \
|
||||
--content '{
|
||||
"zh_cn": {
|
||||
"title": "Meeting reaction",
|
||||
"content": [[
|
||||
{"tag": "text", "text": "Reaction: "},
|
||||
{"tag": "emotion", "emoji_type": "JIAYI"}
|
||||
]]
|
||||
}
|
||||
}' \
|
||||
--as bot
|
||||
```
|
||||
|
||||
Use the exact key from `.chat_emoji_types[]` as `emoji_type`; do not convert it to a system emoji or synthesize another value.
|
||||
|
||||
Reference in New Issue
Block a user