fix(vc): rename meeting events participants field

This commit is contained in:
renaocheng
2026-07-06 14:54:30 +08:00
parent 4859c5da62
commit 7c4f500f66
4 changed files with 77 additions and 77 deletions

View File

@@ -103,15 +103,15 @@ var VCMeetingEvents = common.Shortcut{
}
events = compactMeetingEvents(events)
identity, identityWarning := meetingEventsCurrentIdentity(runtime)
currentRoster, rosterWarning := fetchMeetingEventsCurrentRoster(runtime)
outData := buildMeetingEventsOutput(data, events, currentRoster, identity, identityWarning, rosterWarning)
currentParticipants, participantsWarning := fetchMeetingEventsCurrentParticipants(runtime)
outData := buildMeetingEventsOutput(data, events, currentParticipants, identity, identityWarning, participantsWarning)
metadata := map[string]interface{}{
"row_type": "metadata",
"meeting": outData.Meeting,
"identity": outData.Identity,
"current_roster": outData.CurrentRoster,
"has_more": outData.HasMore,
"page_token": outData.PageToken,
"row_type": "metadata",
"meeting": outData.Meeting,
"identity": outData.Identity,
"current_participants": outData.CurrentParticipants,
"has_more": outData.HasMore,
"page_token": outData.PageToken,
}
if len(outData.Warnings) > 0 {
metadata["warnings"] = outData.Warnings
@@ -137,13 +137,13 @@ var VCMeetingEvents = common.Shortcut{
}
type meetingEventsOutput struct {
Meeting meetingEventsMeeting `json:"meeting"`
Identity meetingEventsIdentity `json:"identity"`
CurrentRoster []meetingEventsIdentity `json:"current_roster"`
Events []meetingEventsEvent `json:"events"`
Warnings []string `json:"warnings,omitempty"`
HasMore bool `json:"has_more"`
PageToken string `json:"page_token,omitempty"`
Meeting meetingEventsMeeting `json:"meeting"`
Identity meetingEventsIdentity `json:"identity"`
CurrentParticipants []meetingEventsIdentity `json:"current_participants"`
Events []meetingEventsEvent `json:"events"`
Warnings []string `json:"warnings,omitempty"`
HasMore bool `json:"has_more"`
PageToken string `json:"page_token,omitempty"`
}
type meetingEventsMeeting struct {
@@ -172,7 +172,7 @@ type meetingEventsEvent struct {
Payload map[string]interface{} `json:"payload,omitempty"`
}
func buildMeetingEventsOutput(data map[string]interface{}, events []interface{}, currentRoster []interface{}, identity meetingEventsIdentity, warnings ...string) meetingEventsOutput {
func buildMeetingEventsOutput(data map[string]interface{}, events []interface{}, currentParticipants []interface{}, identity meetingEventsIdentity, warnings ...string) meetingEventsOutput {
output := meetingEventsOutput{
Meeting: meetingEventsMeetingFromPayload(nil),
Identity: identity,
@@ -195,7 +195,7 @@ func buildMeetingEventsOutput(data map[string]interface{}, events []interface{},
}
output.Events = append(output.Events, meetingEventsEventFromPayload(event, output.Identity))
}
output.CurrentRoster = enrichMeetingEventsCurrentRoster(meetingEventsCurrentRoster(currentRoster, output.Identity), output.Events)
output.CurrentParticipants = enrichMeetingEventsCurrentParticipants(meetingEventsCurrentParticipants(currentParticipants, output.Identity), output.Events)
return output
}
@@ -222,12 +222,12 @@ func meetingEventsCurrentIdentity(runtime *common.RuntimeContext) (meetingEvents
return identity, ""
}
func fetchMeetingEventsCurrentRoster(runtime *common.RuntimeContext) ([]interface{}, string) {
func fetchMeetingEventsCurrentParticipants(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", "user_id_type": "open_id"}, nil)
if err != nil {
return nil, fmt.Sprintf("current_roster unavailable: %v", err)
return nil, fmt.Sprintf("current_participants unavailable: %v", err)
}
if meeting := common.GetMap(data, "meeting"); meeting != nil {
return common.GetSlice(meeting, "participants"), ""
@@ -284,9 +284,9 @@ func meetingEventsEventFromPayload(event map[string]interface{}, selfIdentity me
return out
}
func meetingEventsCurrentRoster(rawRoster []interface{}, selfIdentity meetingEventsIdentity) []meetingEventsIdentity {
roster := make([]meetingEventsIdentity, 0, len(rawRoster))
for _, raw := range rawRoster {
func meetingEventsCurrentParticipants(rawParticipants []interface{}, selfIdentity meetingEventsIdentity) []meetingEventsIdentity {
participants := make([]meetingEventsIdentity, 0, len(rawParticipants))
for _, raw := range rawParticipants {
item, _ := raw.(map[string]interface{})
if item == nil {
continue
@@ -295,12 +295,12 @@ func meetingEventsCurrentRoster(rawRoster []interface{}, selfIdentity meetingEve
if nested := common.GetMap(item, "participant"); nested != nil {
participant = nested
}
roster = append(roster, meetingEventsIdentityFromParticipant(participant, selfIdentity))
participants = append(participants, meetingEventsIdentityFromParticipant(participant, selfIdentity))
}
return roster
return participants
}
func enrichMeetingEventsCurrentRoster(roster []meetingEventsIdentity, events []meetingEventsEvent) []meetingEventsIdentity {
func enrichMeetingEventsCurrentParticipants(participants []meetingEventsIdentity, events []meetingEventsEvent) []meetingEventsIdentity {
typeByID := make(map[string]string)
nameByID := make(map[string]string)
for _, event := range events {
@@ -320,25 +320,25 @@ func enrichMeetingEventsCurrentRoster(roster []meetingEventsIdentity, events []m
}
}
}
for i := range roster {
for i := range participants {
changed := false
if roster[i].Name == "" {
if name := nameByID[roster[i].ID]; name != "" {
roster[i].Name = name
if participants[i].Name == "" {
if name := nameByID[participants[i].ID]; name != "" {
participants[i].Name = name
changed = true
}
}
if isUnknownParticipantType(roster[i].ParticipantType) {
if participantType := typeByID[roster[i].ID]; participantType != "" {
roster[i].ParticipantType = participantType
if isUnknownParticipantType(participants[i].ParticipantType) {
if participantType := typeByID[participants[i].ID]; participantType != "" {
participants[i].ParticipantType = participantType
changed = true
}
}
if changed {
roster[i].Label = identityLabel(roster[i])
participants[i].Label = identityLabel(participants[i])
}
}
return roster
return participants
}
func eventActors(eventType string, payload map[string]interface{}, selfIdentity meetingEventsIdentity) []meetingEventsIdentity {
@@ -419,7 +419,7 @@ func meetingEventsParticipantTypeFromParticipantType(raw string) string {
}
func meetingEventsParticipantRole(participant map[string]interface{}) string {
if raw := meetingEventsRoleFromRosterRole(fieldValueString(participant, "role")); raw != "" {
if raw := meetingEventsRoleFromParticipantRole(fieldValueString(participant, "role")); raw != "" {
return raw
}
return meetingEventsRoleFromEventUserRole(fieldValueString(participant, "user_role"))
@@ -452,7 +452,7 @@ func isUnknownParticipantType(participantType string) bool {
return participantType == "" || participantType == "unknown"
}
func meetingEventsRoleFromRosterRole(raw string) string {
func meetingEventsRoleFromParticipantRole(raw string) string {
raw = strings.ToLower(strings.TrimSpace(raw))
switch raw {
case "1", "host":
@@ -580,9 +580,9 @@ func renderMeetingEventsCompactPretty(w io.Writer, data meetingEventsOutput, tim
if data.Identity.Label != "" {
fmt.Fprintf(w, "当前身份:%s\n", escapePrettyText(data.Identity.Label))
}
if len(data.CurrentRoster) > 0 {
if len(data.CurrentParticipants) > 0 {
fmt.Fprintln(w, "当前名单:")
for _, participant := range data.CurrentRoster {
for _, participant := range data.CurrentParticipants {
fmt.Fprintf(w, "- %s\n", escapePrettyText(participant.Label))
}
fmt.Fprintln(w)

View File

@@ -82,7 +82,7 @@ func botInfoErrorStub() *httpmock.Stub {
}
}
func meetingDetailRosterStub(roster []interface{}) *httpmock.Stub {
func meetingDetailParticipantsStub(participants []interface{}) *httpmock.Stub {
return &httpmock.Stub{
Method: "GET",
URL: "/open-apis/vc/v1/meetings/7628568141510692381",
@@ -92,14 +92,14 @@ func meetingDetailRosterStub(roster []interface{}) *httpmock.Stub {
"data": map[string]interface{}{
"meeting": map[string]interface{}{
"id": "7628568141510692381",
"participants": roster,
"participants": participants,
},
},
},
}
}
func meetingDetailRosterErrorStub() *httpmock.Stub {
func meetingDetailParticipantsErrorStub() *httpmock.Stub {
return &httpmock.Stub{
Method: "GET",
URL: "/open-apis/vc/v1/meetings/7628568141510692381",
@@ -555,7 +555,7 @@ func TestMeetingEvents_ExecuteJSON_PageAll(t *testing.T) {
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "pt_2"))
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, false, ""))
reg.Register(botInfoStub())
reg.Register(meetingDetailRosterStub(nil))
reg.Register(meetingDetailParticipantsStub(nil))
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
@@ -597,7 +597,7 @@ func TestMeetingEvents_ExecuteJSON(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "1710000000000000000"))
reg.Register(botInfoStub())
reg.Register(meetingDetailRosterStub([]interface{}{
reg.Register(meetingDetailParticipantsStub([]interface{}{
map[string]interface{}{"id": "bot_001", "user_name": "Demo Bot", "participant_type": "2", "role": "4"},
map[string]interface{}{"id": "u1", "user_name": "Alice", "participant_type": "1", "role": "1"},
}))
@@ -617,7 +617,7 @@ func TestMeetingEvents_ExecuteJSON(t *testing.T) {
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]"}`,
`"current_roster":[`,
`"current_participants":[`,
`"role":"host"`,
`"is_self":true`,
`"event_type":"participant_joined"`,
@@ -641,11 +641,11 @@ func TestMeetingEvents_ExecuteJSON(t *testing.T) {
}
}
func TestMeetingEvents_ExecuteJSON_RosterErrorDoesNotBlockEvents(t *testing.T) {
func TestMeetingEvents_ExecuteJSON_ParticipantsErrorDoesNotBlockEvents(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, false, ""))
reg.Register(botInfoStub())
reg.Register(meetingDetailRosterErrorStub())
reg.Register(meetingDetailParticipantsErrorStub())
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
@@ -662,9 +662,9 @@ func TestMeetingEvents_ExecuteJSON_RosterErrorDoesNotBlockEvents(t *testing.T) {
out = strings.ReplaceAll(out, "\n", "")
for _, want := range []string{
`"event_type":"participant_joined"`,
`"current_roster":[]`,
`"current_participants":[]`,
`"warnings":[`,
`current_rosterunavailable`,
`current_participantsunavailable`,
} {
if !strings.Contains(out, want) {
t.Fatalf("json output missing %q: %s", want, stdout.String())
@@ -676,7 +676,7 @@ func TestMeetingEvents_ExecuteJSON_BotIdentityErrorDoesNotBlockEvents(t *testing
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, false, ""))
reg.Register(botInfoErrorStub())
reg.Register(meetingDetailRosterStub(nil))
reg.Register(meetingDetailParticipantsStub(nil))
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
@@ -706,7 +706,7 @@ func TestMeetingEvents_ExecuteJSON_BotIdentityErrorDoesNotBlockEvents(t *testing
func TestMeetingEvents_ExecuteJSON_UserIdentitySkipsBotInfo(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, false, ""))
reg.Register(meetingDetailRosterStub([]interface{}{
reg.Register(meetingDetailParticipantsStub([]interface{}{
map[string]interface{}{"id": "ou_testuser", "user_name": "Current User", "participant_type": "1", "role": "1"},
map[string]interface{}{"id": "u1", "user_name": "Alice", "participant_type": "1", "role": "1"},
}))
@@ -726,7 +726,7 @@ func TestMeetingEvents_ExecuteJSON_UserIdentitySkipsBotInfo(t *testing.T) {
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]"}`,
`"current_roster":[`,
`"current_participants":[`,
`"id":"ou_testuser"`,
`"is_self":true`,
`"event_type":"participant_joined"`,
@@ -742,7 +742,7 @@ func TestMeetingEvents_ExecuteJSON_OngoingMeetingOmitsEndTime(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEventOngoing()}, false, ""))
reg.Register(botInfoStub())
reg.Register(meetingDetailRosterStub(nil))
reg.Register(meetingDetailParticipantsStub(nil))
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
@@ -798,7 +798,7 @@ func TestMeetingEvents_ExecuteNDJSONIncludesMetadataRow(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEvent()}, true, "1710000000000000000"))
reg.Register(botInfoStub())
reg.Register(meetingDetailRosterStub([]interface{}{
reg.Register(meetingDetailParticipantsStub([]interface{}{
map[string]interface{}{"id": "bot_001", "user_name": "Demo Bot", "participant_type": "2", "role": "4"},
}))
@@ -833,7 +833,7 @@ func TestMeetingEvents_ExecuteNDJSONIncludesMetadataRow(t *testing.T) {
`"has_more":true`,
`"page_token":"1710000000000000000"`,
`"identity":`,
`"current_roster":[`,
`"current_participants":[`,
} {
if !strings.Contains(lines[1], want) {
t.Fatalf("metadata ndjson row missing %q: %s", want, lines[1])
@@ -845,7 +845,7 @@ func TestMeetingEvents_ExecuteJSON_PrunesEmptySlices(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{chatReceivedEvent()}, false, ""))
reg.Register(botInfoStub())
reg.Register(meetingDetailRosterStub(nil))
reg.Register(meetingDetailParticipantsStub(nil))
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
@@ -879,7 +879,7 @@ func TestMeetingEvents_ExecuteJSON_PreservesReactionItems(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{mixedChatAndReactionEvent()}, false, ""))
reg.Register(botInfoStub())
reg.Register(meetingDetailRosterStub(nil))
reg.Register(meetingDetailParticipantsStub(nil))
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
@@ -913,7 +913,7 @@ func TestMeetingEvents_ExecutePretty(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEventOngoing(), multiChatReceivedEvent(), magicShareStartedEvent()}, true, "1710000000000000000"))
reg.Register(botInfoStub())
reg.Register(meetingDetailRosterStub([]interface{}{
reg.Register(meetingDetailParticipantsStub([]interface{}{
map[string]interface{}{"id": "bot_001", "user_name": "Demo Bot", "participant_type": "2", "role": "4"},
map[string]interface{}{"id": "u1", "user_name": "Alice", "participant_type": "1", "role": "1"},
}))
@@ -960,7 +960,7 @@ func TestMeetingEvents_ExecutePretty_PrintsPageTokenWithoutHasMore(t *testing.T)
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub([]interface{}{participantJoinedEventOngoing()}, false, "pt_last"))
reg.Register(botInfoStub())
reg.Register(meetingDetailRosterStub(nil))
reg.Register(meetingDetailParticipantsStub(nil))
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
@@ -986,7 +986,7 @@ func TestMeetingEvents_ExecuteEmpty(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
reg.Register(meetingEventsStub(nil, false, ""))
reg.Register(botInfoStub())
reg.Register(meetingDetailRosterStub(nil))
reg.Register(meetingDetailParticipantsStub(nil))
err := mountAndRun(t, VCMeetingEvents, []string{
"+meeting-events",
@@ -1317,7 +1317,7 @@ func TestMeetingEventsIdentityFromParticipant_UnknownUserType(t *testing.T) {
}
}
func TestBuildMeetingEventsOutput_EnrichesUnknownRosterTypeFromActors(t *testing.T) {
func TestBuildMeetingEventsOutput_EnrichesUnknownParticipantTypeFromActors(t *testing.T) {
event := participantJoinedEvent()
payload := common.GetMap(event, "payload")
items := common.GetSlice(payload, "participant_joined_items")
@@ -1333,21 +1333,21 @@ func TestBuildMeetingEventsOutput_EnrichesUnknownRosterTypeFromActors(t *testing
map[string]interface{}{"id": "ou_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 := len(out.CurrentParticipants); got != 1 {
t.Fatalf("current_participants 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.CurrentParticipants[0].ParticipantType; got != "bot" {
t.Fatalf("current_participants participant_type = %q, want bot: %#v", got, out.CurrentParticipants[0])
}
if got := out.CurrentRoster[0].Name; got != "Meeting Bot" {
t.Fatalf("current_roster name = %q, want actor name", got)
if got := out.CurrentParticipants[0].Name; got != "Meeting Bot" {
t.Fatalf("current_participants name = %q, want actor name", got)
}
if got := out.CurrentRoster[0].Label; got != "Meeting Bot [bot,participant]" {
t.Fatalf("current_roster label = %q, want enriched bot label", got)
if got := out.CurrentParticipants[0].Label; got != "Meeting Bot [bot,participant]" {
t.Fatalf("current_participants label = %q, want enriched bot label", got)
}
}
func TestBuildMeetingEventsOutput_DoesNotOverwriteRosterName(t *testing.T) {
func TestBuildMeetingEventsOutput_DoesNotOverwriteParticipantName(t *testing.T) {
event := participantJoinedEvent()
payload := common.GetMap(event, "payload")
items := common.GetSlice(payload, "participant_joined_items")
@@ -1360,14 +1360,14 @@ func TestBuildMeetingEventsOutput_DoesNotOverwriteRosterName(t *testing.T) {
}
out := buildMeetingEventsOutput(map[string]interface{}{}, []interface{}{event}, []interface{}{
map[string]interface{}{"id": "ou_user", "user_name": "Roster Name", "user_type": 1, "role": "3"},
map[string]interface{}{"id": "ou_user", "user_name": "Participant Name", "user_type": 1, "role": "3"},
}, meetingEventsIdentity{})
if got := out.CurrentRoster[0].Name; got != "Roster Name" {
t.Fatalf("current_roster name = %q, want roster name", got)
if got := out.CurrentParticipants[0].Name; got != "Participant Name" {
t.Fatalf("current_participants name = %q, want participant name", got)
}
if got := out.CurrentRoster[0].Label; got != "Roster Name [human,participant]" {
t.Fatalf("current_roster label = %q, want roster label", got)
if got := out.CurrentParticipants[0].Label; got != "Participant Name [human,participant]" {
t.Fatalf("current_participants label = %q, want participant label", got)
}
}

View File

@@ -72,7 +72,7 @@ metadata:
- 再根据 `note_id``minute_token` 和用户意图,按 [`lark-vc`](../lark-vc/SKILL.md) 的产物决策读取正文、逐字稿或妙记。
- 想看参会人快照:用 `vc meeting get --with-participants`(见 [`lark-vc`](../lark-vc/SKILL.md)
5. **默认必须使用** **`--page-all`**,除非用户明确要求“只查一页”,或确实需要控制返回体大小。
6. 命令默认输出结构化事件契约:`meeting``identity``current_roster``events``warnings``has_more``page_token`;参会人含 `participant_type``role``is_self` 和可读 `label`,事件细节保留在 `payload`
6. 命令默认输出结构化事件契约:`meeting``identity``current_participants``events``warnings``has_more``page_token`;参会人含 `participant_type``role``is_self` 和可读 `label`,事件细节保留在 `payload`
7. 输出格式默认优先 `--format pretty`(时间线更易读,并带当前身份与当前名单标签);需要稳定字段做结构化处理时用 `--format json`;需要流式消费事件时用 `--format ndjson`
8. **必须识别分页信号**:只要响应里出现 `has_more=true`、pretty 里的 `more available`,或返回了非空 `page_token`,就不能把当前结果当作完整事件流;默认应继续分页,或明确告诉用户当前只是部分结果。
9. 保留响应里的 `page_token`,下次增量拉取直接续,不要从头再拉。

View File

@@ -110,7 +110,7 @@ lark-cli vc +meeting-events --as user --meeting-id <id> --page-all --format pret
### 5. 输出格式差异
- `--format json`:结构化契约,顶层包含 `meeting``identity``current_roster``events``has_more``page_token`。参会人身份统一含 `participant_type``role``is_self``label`;每条事件保留 `payload` 便于追溯细节。
- `--format json`:结构化契约,顶层包含 `meeting``identity``current_participants``events``has_more``page_token`。参会人身份统一含 `participant_type``role``is_self``label`;每条事件保留 `payload` 便于追溯细节。
- `--format pretty`:默认推荐格式,输出当前身份、当前名单和逐条时间线,适合快速理解“发生了什么”。
- `--format ndjson`:输出事件行,并带 metadata 行,适合流式消费。
@@ -160,7 +160,7 @@ lark-cli vc +meeting-events --as user --meeting-id <id> --page-all --format pret
|------|------|
| `meeting` | 会议身份与时间状态,包含 `id/topic/meeting_no/start_time/end_time/status` |
| `identity` | 当前读取身份,包含 `id/name/participant_type/role/is_self/label` |
| `current_roster` | 服务端当前名单;当 roster 缺少姓名或类型为 `unknown`CLI 会用同 ID 的事件 actor 信息补全,不做完整历史 replay |
| `current_participants` | 服务端当前参会人名单;当参会人缺少姓名或类型为 `unknown`CLI 会用同 ID 的事件 actor 信息补全,不做完整历史 replay |
| `events` | 结构化事件列表;每条事件含 `actors` 和事件细节 `payload` |
| `warnings` | 非阻断告警列表,例如当前名单补充信息不可用;事件列表本身仍可使用 |
| `has_more` | 是否还有下一页 |