fix(vc): simplify meeting events output contract

This commit is contained in:
renaocheng
2026-07-06 14:18:41 +08:00
parent 061ded686d
commit 264f5b2d2b
2 changed files with 124 additions and 19 deletions

View File

@@ -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)
}

View File

@@ -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",