From 264f5b2d2bcd38013dd430ef3cfaeabee1e514c9 Mon Sep 17 00:00:00 2001 From: renaocheng Date: Mon, 6 Jul 2026 14:18:41 +0800 Subject: [PATCH] fix(vc): simplify meeting events output contract --- shortcuts/vc/vc_meeting_events.go | 61 +++++++++++++++---- shortcuts/vc/vc_meeting_events_test.go | 82 ++++++++++++++++++++++++-- 2 files changed, 124 insertions(+), 19 deletions(-) diff --git a/shortcuts/vc/vc_meeting_events.go b/shortcuts/vc/vc_meeting_events.go index 641322deb..e58dc449a 100644 --- a/shortcuts/vc/vc_meeting_events.go +++ b/shortcuts/vc/vc_meeting_events.go @@ -168,10 +168,8 @@ type meetingEventsEvent struct { EventID string `json:"event_id,omitempty"` EventType string `json:"event_type,omitempty"` EventTime string `json:"event_time,omitempty"` - Summary string `json:"summary,omitempty"` Actors []meetingEventsIdentity `json:"actors,omitempty"` Payload map[string]interface{} `json:"payload,omitempty"` - Raw map[string]interface{} `json:"raw,omitempty"` } func buildMeetingEventsOutput(data map[string]interface{}, events []interface{}, currentRoster []interface{}, identity meetingEventsIdentity, warnings ...string) meetingEventsOutput { @@ -197,7 +195,7 @@ func buildMeetingEventsOutput(data map[string]interface{}, events []interface{}, } output.Events = append(output.Events, meetingEventsEventFromPayload(event, output.Identity)) } - output.CurrentRoster = meetingEventsCurrentRoster(currentRoster, output.Identity) + output.CurrentRoster = enrichMeetingEventsCurrentRoster(meetingEventsCurrentRoster(currentRoster, output.Identity), output.Events) return output } @@ -217,7 +215,7 @@ func meetingEventsCurrentIdentity(runtime *common.RuntimeContext) (meetingEvents Role: "user", IsSelf: true, } - identity.Label = identityLabel(identity) + identity.Label = currentIdentityLabel(identity) if userOpenID == "" { return identity, "identity unavailable: current user open_id is unavailable" } @@ -227,7 +225,7 @@ func meetingEventsCurrentIdentity(runtime *common.RuntimeContext) (meetingEvents func fetchMeetingEventsCurrentRoster(runtime *common.RuntimeContext) ([]interface{}, string) { meetingID := strings.TrimSpace(runtime.Str("meeting-id")) data, err := runtime.CallAPITyped(http.MethodGet, fmt.Sprintf("/open-apis/vc/v1/meetings/%s", validate.EncodePathSegment(meetingID)), - map[string]interface{}{"with_participants": "true", "query_mode": "0"}, nil) + map[string]interface{}{"with_participants": "true", "query_mode": "0", "user_id_type": "open_id"}, nil) if err != nil { return nil, fmt.Sprintf("current_roster unavailable: %v", err) } @@ -248,7 +246,7 @@ func meetingEventsBotIdentity(botInfo *common.BotInfo) meetingEventsIdentity { Role: "bot", IsSelf: true, } - identity.Label = identityLabel(identity) + identity.Label = currentIdentityLabel(identity) return identity } @@ -276,14 +274,11 @@ func meetingEventsMeetingFromPayload(meeting map[string]interface{}) meetingEven func meetingEventsEventFromPayload(event map[string]interface{}, selfIdentity meetingEventsIdentity) meetingEventsEvent { payload := common.GetMap(event, "payload") - rawCopy := cloneStringMap(event) out := meetingEventsEvent{ EventID: common.GetString(event, "event_id"), EventType: meetingEventType(event), EventTime: meetingEventsTimeString(common.GetString(event, "event_time")), - Summary: meetingEventSummary(event), Payload: payload, - Raw: rawCopy, } out.Actors = eventActors(out.EventType, payload, selfIdentity) return out @@ -305,6 +300,30 @@ func meetingEventsCurrentRoster(rawRoster []interface{}, selfIdentity meetingEve return roster } +func enrichMeetingEventsCurrentRoster(roster []meetingEventsIdentity, events []meetingEventsEvent) []meetingEventsIdentity { + typeByID := make(map[string]string) + for _, event := range events { + for _, actor := range event.Actors { + if actor.ID == "" || !isKnownParticipantType(actor.ParticipantType) { + continue + } + if _, exists := typeByID[actor.ID]; !exists { + typeByID[actor.ID] = actor.ParticipantType + } + } + } + for i := range roster { + if !isUnknownParticipantType(roster[i].ParticipantType) { + continue + } + if participantType := typeByID[roster[i].ID]; participantType != "" { + roster[i].ParticipantType = participantType + roster[i].Label = identityLabel(roster[i]) + } + } + return roster +} + func eventActors(eventType string, payload map[string]interface{}, selfIdentity meetingEventsIdentity) []meetingEventsIdentity { var actors []meetingEventsIdentity addFromItems := func(key, participantKey string) { @@ -378,7 +397,7 @@ func meetingEventsParticipantTypeFromParticipantType(raw string) string { case "": return "" default: - return raw + return "unknown" } } @@ -399,10 +418,23 @@ func meetingEventsParticipantTypeFromUserType(raw string) string { case "": return "" default: - return raw + return "unknown" } } +func isKnownParticipantType(participantType string) bool { + switch participantType { + case "human", "bot": + return true + default: + return false + } +} + +func isUnknownParticipantType(participantType string) bool { + return participantType == "" || participantType == "unknown" +} + func meetingEventsRoleFromRosterRole(raw string) string { raw = strings.ToLower(strings.TrimSpace(raw)) switch raw { @@ -481,6 +513,11 @@ func identityLabel(identity meetingEventsIdentity) string { return fmt.Sprintf("%s [%s]", name, strings.Join(tags, ",")) } +func currentIdentityLabel(identity meetingEventsIdentity) string { + identity.IsSelf = false + return identityLabel(identity) +} + func meetingEventsTimeString(raw string) string { if parsed, ok := parseFlexibleTime(raw); ok { return parsed.UTC().Format(time.RFC3339) @@ -511,10 +548,8 @@ func meetingEventsEventRows(events []meetingEventsEvent, metadata map[string]int "event_id": event.EventID, "event_type": event.EventType, "event_time": event.EventTime, - "summary": event.Summary, "actors": event.Actors, "payload": event.Payload, - "raw": event.Raw, } rows = append(rows, row) } diff --git a/shortcuts/vc/vc_meeting_events_test.go b/shortcuts/vc/vc_meeting_events_test.go index 6924ddeb0..2dfa93a3e 100644 --- a/shortcuts/vc/vc_meeting_events_test.go +++ b/shortcuts/vc/vc_meeting_events_test.go @@ -569,11 +569,25 @@ func TestMeetingEvents_ExecuteJSON_PageAll(t *testing.T) { } reg.Verify(t) + var envelope map[string]interface{} + if err := json.Unmarshal([]byte(stdout.String()), &envelope); err != nil { + t.Fatalf("unmarshal stdout: %v: %s", err, stdout.String()) + } + events := common.GetSlice(common.GetMap(envelope, "data"), "events") + if got := len(events); got != 2 { + t.Fatalf("events len = %d, want 2: %s", got, stdout.String()) + } + for _, raw := range events { + event, _ := raw.(map[string]interface{}) + if _, ok := event["summary"]; ok { + t.Fatalf("event should not expose summary: %s", stdout.String()) + } + if _, ok := event["raw"]; ok { + t.Fatalf("event should not expose raw: %s", stdout.String()) + } + } out := strings.ReplaceAll(stdout.String(), " ", "") out = strings.ReplaceAll(out, "\n", "") - if count := strings.Count(out, `"summary":"participantbot_001(DemoBot)joined"`); count != 2 { - t.Fatalf("expected 2 aggregated events, got %d: %s", count, stdout.String()) - } if !strings.Contains(out, `"has_more":false`) { t.Fatalf("expected final has_more=false: %s", stdout.String()) } @@ -602,7 +616,7 @@ func TestMeetingEvents_ExecuteJSON(t *testing.T) { out := strings.ReplaceAll(stdout.String(), " ", "") out = strings.ReplaceAll(out, "\n", "") for _, want := range []string{ - `"identity":{"id":"bot_001","name":"DemoBot","participant_type":"bot","role":"bot","is_self":true,"label":"DemoBot[bot,self]"}`, + `"identity":{"id":"bot_001","name":"DemoBot","participant_type":"bot","role":"bot","is_self":true,"label":"DemoBot[bot]"}`, `"current_roster":[`, `"role":"host"`, `"is_self":true`, @@ -617,6 +631,14 @@ func TestMeetingEvents_ExecuteJSON(t *testing.T) { t.Fatalf("json output missing %q: %s", want, stdout.String()) } } + for _, unwanted := range []string{ + `"summary":`, + `"raw":`, + } { + if strings.Contains(out, unwanted) { + t.Fatalf("json output should not contain %q: %s", unwanted, stdout.String()) + } + } } func TestMeetingEvents_ExecuteJSON_RosterErrorDoesNotBlockEvents(t *testing.T) { @@ -703,7 +725,7 @@ func TestMeetingEvents_ExecuteJSON_UserIdentitySkipsBotInfo(t *testing.T) { out := strings.ReplaceAll(stdout.String(), " ", "") out = strings.ReplaceAll(out, "\n", "") for _, want := range []string{ - `"identity":{"id":"ou_testuser","participant_type":"human","role":"user","is_self":true,"label":"ou_testuser[human,user,self]"}`, + `"identity":{"id":"ou_testuser","participant_type":"human","role":"user","is_self":true,"label":"ou_testuser[human,user]"}`, `"current_roster":[`, `"id":"ou_testuser"`, `"is_self":true`, @@ -798,6 +820,14 @@ func TestMeetingEvents_ExecuteNDJSONIncludesMetadataRow(t *testing.T) { if !strings.Contains(lines[0], `"row_type":"event"`) || !strings.Contains(lines[0], `"event_type":"participant_joined"`) { t.Fatalf("first ndjson row should be event: %s", lines[0]) } + for _, unwanted := range []string{ + `"summary":`, + `"raw":`, + } { + if strings.Contains(lines[0], unwanted) { + t.Fatalf("event ndjson row should not contain %q: %s", unwanted, lines[0]) + } + } for _, want := range []string{ `"row_type":"metadata"`, `"has_more":true`, @@ -901,7 +931,7 @@ func TestMeetingEvents_ExecutePretty(t *testing.T) { out := stdout.String() for _, want := range []string{ - "当前身份:Demo Bot [bot,self]", + "当前身份:Demo Bot [bot]", "当前名单:", "- Demo Bot [bot,self]", "- Alice [human,host]", @@ -1274,6 +1304,46 @@ func TestMeetingEventsIdentityFromParticipant_UserTypeApp(t *testing.T) { } } +func TestMeetingEventsIdentityFromParticipant_UnknownUserType(t *testing.T) { + got := meetingEventsIdentityFromParticipant(map[string]interface{}{ + "id": "u_unknown", + "user_name": "Unknown", + "user_type": 0, + "user_role": 1, + }, meetingEventsIdentity{}) + + if got.ParticipantType != "unknown" { + t.Fatalf("identity = %#v, want participant_type=unknown", got) + } +} + +func TestBuildMeetingEventsOutput_EnrichesUnknownRosterTypeFromActors(t *testing.T) { + event := participantJoinedEvent() + payload := common.GetMap(event, "payload") + items := common.GetSlice(payload, "participant_joined_items") + item, _ := items[0].(map[string]interface{}) + item["participant"] = map[string]interface{}{ + "id": "ou_bot", + "user_name": "Meeting Bot", + "user_type": 10, + "user_role": 1, + } + + out := buildMeetingEventsOutput(map[string]interface{}{}, []interface{}{event}, []interface{}{ + map[string]interface{}{"id": "ou_bot", "user_name": "Meeting Bot", "user_type": 0, "role": "3"}, + }, meetingEventsIdentity{}) + + if got := len(out.CurrentRoster); got != 1 { + t.Fatalf("current_roster len = %d, want 1", got) + } + if got := out.CurrentRoster[0].ParticipantType; got != "bot" { + t.Fatalf("current_roster participant_type = %q, want bot: %#v", got, out.CurrentRoster[0]) + } + if got := out.CurrentRoster[0].Label; got != "Meeting Bot [bot,participant]" { + t.Fatalf("current_roster label = %q, want enriched bot label", got) + } +} + func TestMeetingEventsIdentityFromParticipant_IgnoresGenericTypeField(t *testing.T) { got := meetingEventsIdentityFromParticipant(map[string]interface{}{ "id": "u1",