feat(vc): add recording event support (#1369)

This commit is contained in:
calendar-assistant
2026-06-10 11:42:12 +08:00
committed by GitHub
parent 7fdf55821b
commit 31bc87a2cc
5 changed files with 858 additions and 6 deletions

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package vc
import (
"context"
"encoding/json"
"strconv"
"time"
"github.com/larksuite/cli/internal/event"
)
// VCRecordingEndedOutput is the flattened shape for vc.recording.recording_ended_v1.
type VCRecordingEndedOutput struct {
Type string `json:"type" desc:"Event type; always vc.recording.recording_ended_v1"`
EventID string `json:"event_id,omitempty" desc:"Globally unique event ID; safe for deduplication"`
EventTime string `json:"event_time,omitempty" desc:"Time when the recording ended and uploaded successfully, in RFC3339 / ISO 8601 with the current system timezone"`
UniqueKey string `json:"unique_key,omitempty" desc:"Unique key generated for one recording_bean recording session"`
Source string `json:"source,omitempty" desc:"Recording source; always recording_bean"`
}
type recordingEndedEnvelope struct {
Header struct {
EventID string `json:"event_id"`
EventType string `json:"event_type"`
CreateTime string `json:"create_time"`
} `json:"header"`
Event recordingEndedEvent `json:"event"`
}
type recordingEndedEvent struct {
UniqueKey string `json:"unique_key"`
Source string `json:"source"`
}
func processVCRecordingEnded(_ context.Context, _ event.APIClient, raw *event.RawEvent, _ map[string]string) (json.RawMessage, error) {
envelope, ok := parseRecordingEndedEnvelope(raw)
if !ok {
return raw.Payload, nil
}
if !isRecordingEndedBeanEvent(envelope) {
return nil, nil
}
out := &VCRecordingEndedOutput{
Type: recordingEndedEventType(envelope, raw),
EventID: envelope.Header.EventID,
EventTime: recordingEndedEventTime(envelope.Header.CreateTime),
UniqueKey: envelope.Event.UniqueKey,
Source: envelope.Event.Source,
}
return json.Marshal(out)
}
func parseRecordingEndedEnvelope(raw *event.RawEvent) (*recordingEndedEnvelope, bool) {
var envelope recordingEndedEnvelope
if err := json.Unmarshal(raw.Payload, &envelope); err != nil {
return nil, false
}
return &envelope, true
}
func isRecordingEndedBeanEvent(envelope *recordingEndedEnvelope) bool {
return envelope != nil && envelope.Event.Source == "recording_bean"
}
func recordingEndedEventType(envelope *recordingEndedEnvelope, raw *event.RawEvent) string {
if envelope != nil && envelope.Header.EventType != "" {
return envelope.Header.EventType
}
return raw.EventType
}
func recordingEndedEventTime(raw string) string {
if raw == "" {
return ""
}
millis, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return ""
}
return time.UnixMilli(millis).Local().Format(time.RFC3339)
}

View File

@@ -0,0 +1,84 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package vc
import (
"context"
"encoding/json"
"strconv"
"time"
"github.com/larksuite/cli/internal/event"
)
// VCRecordingStartedOutput is the flattened shape for vc.recording.recording_started_v1.
type VCRecordingStartedOutput struct {
Type string `json:"type" desc:"Event type; always vc.recording.recording_started_v1"`
EventID string `json:"event_id,omitempty" desc:"Globally unique event ID; safe for deduplication"`
EventTime string `json:"event_time,omitempty" desc:"Recording start time in RFC3339 / ISO 8601 with the current system timezone"`
UniqueKey string `json:"unique_key,omitempty" desc:"Unique key generated for one recording_bean recording session"`
Source string `json:"source,omitempty" desc:"Recording source; always recording_bean"`
}
type recordingStartedEnvelope struct {
Header struct {
EventID string `json:"event_id"`
EventType string `json:"event_type"`
CreateTime string `json:"create_time"`
} `json:"header"`
Event recordingStartedEvent `json:"event"`
}
type recordingStartedEvent struct {
UniqueKey string `json:"unique_key"`
Source string `json:"source"`
}
func processVCRecordingStarted(_ context.Context, _ event.APIClient, raw *event.RawEvent, _ map[string]string) (json.RawMessage, error) {
envelope, ok := parseRecordingStartedEnvelope(raw)
if !ok {
return raw.Payload, nil
}
if !isRecordingStartedBeanEvent(envelope) {
return nil, nil
}
out := &VCRecordingStartedOutput{
Type: recordingStartedEventType(envelope, raw),
EventID: envelope.Header.EventID,
EventTime: recordingStartedEventTime(envelope.Header.CreateTime),
UniqueKey: envelope.Event.UniqueKey,
Source: envelope.Event.Source,
}
return json.Marshal(out)
}
func parseRecordingStartedEnvelope(raw *event.RawEvent) (*recordingStartedEnvelope, bool) {
var envelope recordingStartedEnvelope
if err := json.Unmarshal(raw.Payload, &envelope); err != nil {
return nil, false
}
return &envelope, true
}
func isRecordingStartedBeanEvent(envelope *recordingStartedEnvelope) bool {
return envelope != nil && envelope.Event.Source == "recording_bean"
}
func recordingStartedEventType(envelope *recordingStartedEnvelope, raw *event.RawEvent) string {
if envelope != nil && envelope.Header.EventType != "" {
return envelope.Header.EventType
}
return raw.EventType
}
func recordingStartedEventTime(raw string) string {
if raw == "" {
return ""
}
millis, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return ""
}
return time.UnixMilli(millis).Local().Format(time.RFC3339)
}

468
events/vc/recording_test.go Normal file
View File

@@ -0,0 +1,468 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package vc
import (
"context"
"encoding/json"
"reflect"
"strings"
"testing"
"time"
"github.com/larksuite/cli/internal/event"
)
func TestVCKeys_RecordingEventsRegistered(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
for _, tc := range []struct {
eventType string
}{
{eventTypeRecordingStarted},
{eventTypeRecordingTranscriptGenerated},
{eventTypeRecordingEnded},
} {
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:recording: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 !strings.Contains(def.Description, "recording_bean") {
t.Errorf("Description should document recording_bean source, got %q", def.Description)
}
if !strings.Contains(def.Description, "connected to Feishu software") {
t.Errorf("Description should document Feishu software connection requirement, got %q", def.Description)
}
if strings.Contains(def.Description, "future") || strings.Contains(def.Description, "software_recording") {
t.Errorf("Description should not mention future sources, got %q", def.Description)
}
if tc.eventType == eventTypeRecordingEnded && (strings.Contains(def.Description, "object_type") || strings.Contains(def.Description, "object_id")) {
t.Errorf("ended Description should not document object metadata, got %q", def.Description)
}
wantSchemaType := reflect.TypeOf(VCRecordingStartedOutput{})
switch tc.eventType {
case eventTypeRecordingTranscriptGenerated:
wantSchemaType = reflect.TypeOf(VCRecordingTranscriptGeneratedOutput{})
case eventTypeRecordingEnded:
wantSchemaType = reflect.TypeOf(VCRecordingEndedOutput{})
}
if def.Schema.Custom.Type != wantSchemaType {
t.Errorf("Custom schema Type = %v, want %v", def.Schema.Custom.Type, wantSchemaType)
}
})
}
}
func TestProcessVCRecordingStarted(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
out := runRecordingProcess[VCRecordingStartedOutput](t, eventTypeRecordingStarted, processVCRecordingStarted, `{
"schema": "2.0",
"header": {
"event_id": "ev_rec_start_001",
"event_type": "vc.recording.recording_started_v1",
"create_time": "1761782400000"
},
"event": {
"unique_key": "recording_001",
"source": "recording_bean"
}
}`)
if out.Type != eventTypeRecordingStarted {
t.Errorf("Type = %q", out.Type)
}
if out.EventID != "ev_rec_start_001" || out.EventTime != recordingTestEventTime(1761782400000) {
t.Errorf("EventID/EventTime = %q/%q", out.EventID, out.EventTime)
}
if out.UniqueKey != "recording_001" || out.Source != "recording_bean" {
t.Errorf("UniqueKey/Source = %q/%q", out.UniqueKey, out.Source)
}
}
func TestProcessVCRecordingTranscriptGenerated(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
got := runRecordingProcessRaw(t, eventTypeRecordingTranscriptGenerated, processVCRecordingTranscriptGenerated, `{
"schema": "2.0",
"header": {
"event_id": "ev_rec_transcript_001",
"event_type": "vc.recording.recording_transcript_generated_v1",
"create_time": "1761782400100"
},
"event": {
"unique_key": "recording_001",
"source": "recording_bean",
"transcript_items": [
{
"speaker": {
"id": {
"open_id": "ou_0f8bf7acdf2ae69553ecbdbfbbd10a53",
"union_id": "on_bc03f16d781bff4178a5d11e48eb1867",
"user_id": null
},
"user_type": 100,
"user_role": 1,
"user_name": "Alice"
},
"text": "hello world",
"language": "en_us",
"start_time_ms": "1761782399000",
"end_time_ms": "1761782400000",
"sentence_id": "987654321"
},
{
"speaker": {
"user_name": "Bob"
},
"text": "second sentence",
"language": "en_us",
"start_time_ms": "1761782401000",
"end_time_ms": "1761782402000",
"sentence_id": "987654322"
}
]
}
}`)
if got == nil {
t.Fatal("Process output is nil")
}
var out VCRecordingTranscriptGeneratedOutput
if err := json.Unmarshal(got, &out); err != nil {
t.Fatalf("Process output is not valid JSON: %v\nraw=%s", err, string(got))
}
if out.Type != eventTypeRecordingTranscriptGenerated {
t.Errorf("Type = %q", out.Type)
}
if out.UniqueKey != "recording_001" || out.Source != "recording_bean" {
t.Errorf("UniqueKey/Source = %q/%q", out.UniqueKey, out.Source)
}
if out.EventTime != recordingTestEventTime(1761782400100) {
t.Errorf("EventTime = %q", out.EventTime)
}
if len(out.TranscriptItems) != 2 {
t.Fatalf("TranscriptItems len = %d, want 2", len(out.TranscriptItems))
}
item := out.TranscriptItems[0]
if item.SpeakerName != "Alice" || item.Text != "hello world" {
t.Errorf("Transcript speaker/text = %q/%q", item.SpeakerName, item.Text)
}
if item.StartTime != recordingTestEventTime(1761782399000) || item.EndTime != recordingTestEventTime(1761782400000) {
t.Errorf("Transcript timing = %q/%q", item.StartTime, item.EndTime)
}
if item.SentenceID != "987654321" {
t.Errorf("SentenceID = %q, want 987654321", item.SentenceID)
}
if out.TranscriptItems[1].SpeakerName != "Bob" || out.TranscriptItems[1].SentenceID != "987654322" {
t.Errorf("second transcript item = %+v", out.TranscriptItems[1])
}
itemJSON, err := json.Marshal(item)
if err != nil {
t.Fatalf("marshal transcript item: %v", err)
}
var itemFields map[string]any
if err := json.Unmarshal(itemJSON, &itemFields); err != nil {
t.Fatalf("unmarshal transcript item JSON: %v", err)
}
wantItemFields := map[string]bool{
"speaker_name": true,
"text": true,
"start_time": true,
"end_time": true,
"sentence_id": true,
}
for gotField := range itemFields {
if !wantItemFields[gotField] {
t.Errorf("Transcript item should not contain field %q, got %s", gotField, string(itemJSON))
}
}
for wantField := range wantItemFields {
if _, ok := itemFields[wantField]; !ok {
t.Errorf("Transcript item missing field %q, got %s", wantField, string(itemJSON))
}
}
for _, unexpected := range []string{
`"seq_id"`,
`"speaker"`,
`"user_open_id"`,
`"user_type"`,
`"user_role"`,
`"language"`,
`"start_time_ms"`,
`"end_time_ms"`,
`"sequence_id"`,
`"transcript_item"`,
} {
if strings.Contains(string(got), unexpected) {
t.Errorf("Transcript output should not contain %s, got %s", unexpected, string(got))
}
}
if !strings.Contains(string(got), `"sentence_id":"987654321"`) {
t.Errorf("Transcript output should contain sentence_id, got %s", string(got))
}
}
func TestProcessVCRecordingEnded(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
out := runRecordingProcess[VCRecordingEndedOutput](t, eventTypeRecordingEnded, processVCRecordingEnded, `{
"schema": "2.0",
"header": {
"event_id": "ev_rec_end_001",
"event_type": "vc.recording.recording_ended_v1",
"create_time": "1761782400200"
},
"event": {
"unique_key": "recording_001",
"source": "recording_bean",
"object_type": "minutes",
"object_id": "minute_token_001"
}
}`)
if out.Type != eventTypeRecordingEnded {
t.Errorf("Type = %q", out.Type)
}
if out.UniqueKey != "recording_001" || out.Source != "recording_bean" {
t.Errorf("UniqueKey/Source = %q/%q", out.UniqueKey, out.Source)
}
if out.EventTime != recordingTestEventTime(1761782400200) {
t.Errorf("EventTime = %q", out.EventTime)
}
}
func TestProcessVCRecordingEnded_DropsObjectMetadata(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
got := runRecordingProcessRaw(t, eventTypeRecordingEnded, processVCRecordingEnded, `{
"schema": "2.0",
"header": {
"event_id": "ev_rec_end_001",
"event_type": "vc.recording.recording_ended_v1",
"create_time": "1761782400200"
},
"event": {
"unique_key": "recording_001",
"source": "recording_bean",
"object_type": "minutes",
"object_id": "minute_token_001"
}
}`)
if strings.Contains(string(got), "object_type") || strings.Contains(string(got), "object_id") {
t.Fatalf("ended output should drop object metadata, got %s", string(got))
}
}
func TestProcessVCRecording_DropsTimestampField(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
got := runRecordingProcessRaw(t, eventTypeRecordingStarted, processVCRecordingStarted, `{
"schema": "2.0",
"header": {
"event_id": "ev_rec_start_001",
"event_type": "vc.recording.recording_started_v1",
"create_time": "1761782400000"
},
"event": {
"unique_key": "recording_001",
"source": "recording_bean"
}
}`)
if strings.Contains(string(got), `"timestamp"`) {
t.Fatalf("recording output should use event_time instead of timestamp, got %s", string(got))
}
if !strings.Contains(string(got), `"event_time":"`+recordingTestEventTime(1761782400000)+`"`) {
t.Fatalf("recording output should include ISO 8601 event_time, got %s", string(got))
}
}
func TestProcessVCRecording_NonRecordingBeanFiltered(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
for _, tc := range []struct {
name string
eventType string
process event.ProcessFunc
payload string
}{
{
name: "started",
eventType: eventTypeRecordingStarted,
process: processVCRecordingStarted,
payload: `{
"schema": "2.0",
"header": {"event_id": "ev_rec_start_001", "event_type": "vc.recording.recording_started_v1"},
"event": {"unique_key": "recording_001", "source": "software_recording"}
}`,
},
{
name: "transcript",
eventType: eventTypeRecordingTranscriptGenerated,
process: processVCRecordingTranscriptGenerated,
payload: `{
"schema": "2.0",
"header": {"event_id": "ev_rec_transcript_001", "event_type": "vc.recording.recording_transcript_generated_v1"},
"event": {"unique_key": "recording_001", "source": "software_recording", "transcript_items": []}
}`,
},
{
name: "ended",
eventType: eventTypeRecordingEnded,
process: processVCRecordingEnded,
payload: `{
"schema": "2.0",
"header": {"event_id": "ev_rec_end_001", "event_type": "vc.recording.recording_ended_v1"},
"event": {"unique_key": "recording_001", "source": "software_recording"}
}`,
},
} {
t.Run(tc.name, func(t *testing.T) {
got := runRecordingProcessRaw(t, tc.eventType, tc.process, tc.payload)
if got != nil {
t.Fatalf("non-recording_bean event should be filtered, got %s", string(got))
}
})
}
}
func TestProcessVCRecording_MalformedPayloadPassthrough(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: eventTypeRecordingStarted, process: processVCRecordingStarted},
{name: "transcript", eventType: eventTypeRecordingTranscriptGenerated, process: processVCRecordingTranscriptGenerated},
{name: "ended", eventType: eventTypeRecordingEnded, process: processVCRecordingEnded},
} {
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 TestVCRecording_PreConsumeSubscriptionLifecycle(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
for _, tc := range []struct {
eventType string
}{
{eventTypeRecordingStarted},
{eventTypeRecordingTranscriptGenerated},
{eventTypeRecordingEnded},
} {
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)
}
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 != pathRecordingSubscribe {
t.Fatalf("subscribe call = %+v", calls[0])
}
assertSubscriptionRequest(t, calls[0].body, tc.eventType)
cleanup()
if len(calls) != 2 {
t.Fatalf("calls after cleanup = %d, want 2", len(calls))
}
if calls[1].method != "POST" || calls[1].path != pathRecordingUnsubscribe {
t.Fatalf("unsubscribe call = %+v", calls[1])
}
assertSubscriptionRequest(t, calls[1].body, tc.eventType)
})
}
}
func runRecordingProcess[T any](t *testing.T, eventType string, process event.ProcessFunc, payload string) T {
t.Helper()
got := runRecordingProcessRaw(t, eventType, process, payload)
if got == nil {
t.Fatal("Process output is nil")
}
var out T
if err := json.Unmarshal(got, &out); err != nil {
t.Fatalf("Process output is not valid JSON: %v\nraw=%s", err, string(got))
}
return out
}
func runRecordingProcessRaw(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
}
func recordingTestEventTime(millis int64) string {
return time.UnixMilli(millis).Local().Format(time.RFC3339)
}

View File

@@ -0,0 +1,163 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package vc
import (
"context"
"encoding/json"
"strconv"
"time"
"github.com/larksuite/cli/internal/event"
)
// VCRecordingTranscriptItemOutput is one flattened transcript item for recording events.
type VCRecordingTranscriptItemOutput struct {
SpeakerName string `json:"speaker_name,omitempty" desc:"Speaker display name"`
Text string `json:"text,omitempty" desc:"Transcript text"`
StartTime string `json:"start_time,omitempty" desc:"Transcript item start time in RFC3339 / ISO 8601 with the current system timezone"`
EndTime string `json:"end_time,omitempty" desc:"Transcript item end time in RFC3339 / ISO 8601 with the current system timezone"`
SentenceID string `json:"sentence_id,omitempty" desc:"Transcript sentence ID"`
}
// VCRecordingTranscriptGeneratedOutput is the flattened shape for vc.recording.recording_transcript_generated_v1.
type VCRecordingTranscriptGeneratedOutput struct {
Type string `json:"type" desc:"Event type; always vc.recording.recording_transcript_generated_v1"`
EventID string `json:"event_id,omitempty" desc:"Globally unique event ID; safe for deduplication"`
EventTime string `json:"event_time,omitempty" desc:"Time when this batch of transcript items was generated, in RFC3339 / ISO 8601 with the current system timezone"`
UniqueKey string `json:"unique_key,omitempty" desc:"Unique key generated for one recording_bean recording session"`
Source string `json:"source,omitempty" desc:"Recording source; always recording_bean"`
TranscriptItems []VCRecordingTranscriptItemOutput `json:"transcript_items,omitempty" desc:"Generated transcript items"`
}
type recordingTranscriptGeneratedEnvelope struct {
Header struct {
EventID string `json:"event_id"`
EventType string `json:"event_type"`
CreateTime string `json:"create_time"`
} `json:"header"`
Event recordingTranscriptGeneratedEvent `json:"event"`
}
type recordingTranscriptGeneratedEvent struct {
UniqueKey string `json:"unique_key"`
Source string `json:"source"`
TranscriptItems []recordingTranscriptGeneratedItemIn `json:"transcript_items"`
}
type recordingTranscriptGeneratedItemIn struct {
Speaker *recordingTranscriptGeneratedSpeakerIn `json:"speaker"`
Text string `json:"text"`
StartTimeMs recordingTranscriptGeneratedString `json:"start_time_ms"`
EndTimeMs recordingTranscriptGeneratedString `json:"end_time_ms"`
SentenceID string `json:"sentence_id"`
}
type recordingTranscriptGeneratedSpeakerIn struct {
UserName string `json:"user_name"`
}
type recordingTranscriptGeneratedString string
func processVCRecordingTranscriptGenerated(_ context.Context, _ event.APIClient, raw *event.RawEvent, _ map[string]string) (json.RawMessage, error) {
envelope, ok := parseRecordingTranscriptGeneratedEnvelope(raw)
if !ok {
return raw.Payload, nil
}
if !isRecordingTranscriptGeneratedBeanEvent(envelope) {
return nil, nil
}
out := &VCRecordingTranscriptGeneratedOutput{
Type: recordingTranscriptGeneratedEventType(envelope, raw),
EventID: envelope.Header.EventID,
EventTime: recordingTranscriptGeneratedEventTime(envelope.Header.CreateTime),
UniqueKey: envelope.Event.UniqueKey,
Source: envelope.Event.Source,
TranscriptItems: recordingTranscriptItems(envelope.Event.TranscriptItems),
}
return json.Marshal(out)
}
func parseRecordingTranscriptGeneratedEnvelope(raw *event.RawEvent) (*recordingTranscriptGeneratedEnvelope, bool) {
var envelope recordingTranscriptGeneratedEnvelope
if err := json.Unmarshal(raw.Payload, &envelope); err != nil {
return nil, false
}
return &envelope, true
}
func isRecordingTranscriptGeneratedBeanEvent(envelope *recordingTranscriptGeneratedEnvelope) bool {
return envelope != nil && envelope.Event.Source == "recording_bean"
}
func recordingTranscriptGeneratedEventType(envelope *recordingTranscriptGeneratedEnvelope, raw *event.RawEvent) string {
if envelope != nil && envelope.Header.EventType != "" {
return envelope.Header.EventType
}
return raw.EventType
}
func recordingTranscriptGeneratedEventTime(raw string) string {
return recordingTranscriptGeneratedMillisToLocalRFC3339(raw)
}
func recordingTranscriptGeneratedMillisToLocalRFC3339(raw string) string {
if raw == "" {
return ""
}
millis, err := strconv.ParseInt(raw, 10, 64)
if err != nil {
return ""
}
return time.UnixMilli(millis).Local().Format(time.RFC3339)
}
func recordingTranscriptItems(items []recordingTranscriptGeneratedItemIn) []VCRecordingTranscriptItemOutput {
if len(items) == 0 {
return nil
}
out := make([]VCRecordingTranscriptItemOutput, 0, len(items))
for _, item := range items {
out = append(out, recordingTranscriptItem(item))
}
return out
}
func recordingTranscriptItem(item recordingTranscriptGeneratedItemIn) VCRecordingTranscriptItemOutput {
return VCRecordingTranscriptItemOutput{
SpeakerName: recordingSpeakerName(item.Speaker),
Text: item.Text,
StartTime: recordingTranscriptGeneratedMillisToLocalRFC3339(item.StartTimeMs.String()),
EndTime: recordingTranscriptGeneratedMillisToLocalRFC3339(item.EndTimeMs.String()),
SentenceID: item.SentenceID,
}
}
func recordingSpeakerName(speaker *recordingTranscriptGeneratedSpeakerIn) string {
if speaker == nil {
return ""
}
return speaker.UserName
}
func (s *recordingTranscriptGeneratedString) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
return nil
}
var str string
if err := json.Unmarshal(data, &str); err == nil {
*s = recordingTranscriptGeneratedString(str)
return nil
}
var num json.Number
if err := json.Unmarshal(data, &num); err != nil {
return err
}
*s = recordingTranscriptGeneratedString(num.String())
return nil
}
func (s recordingTranscriptGeneratedString) String() string {
return string(s)
}

View File

@@ -11,13 +11,18 @@ import (
)
const (
eventTypeMeetingEnded = "vc.meeting.participant_meeting_ended_v1"
eventTypeNoteGenerated = "vc.note.generated_v1"
eventTypeMeetingEnded = "vc.meeting.participant_meeting_ended_v1"
eventTypeNoteGenerated = "vc.note.generated_v1"
eventTypeRecordingStarted = "vc.recording.recording_started_v1"
eventTypeRecordingTranscriptGenerated = "vc.recording.recording_transcript_generated_v1"
eventTypeRecordingEnded = "vc.recording.recording_ended_v1"
pathMeetingSubscribe = "/open-apis/vc/v1/meetings/subscription"
pathMeetingUnsubscribe = "/open-apis/vc/v1/meetings/unsubscription"
pathNoteSubscribe = "/open-apis/vc/v1/notes/subscription"
pathNoteUnsubscribe = "/open-apis/vc/v1/notes/unsubscription"
pathMeetingSubscribe = "/open-apis/vc/v1/meetings/subscription"
pathMeetingUnsubscribe = "/open-apis/vc/v1/meetings/unsubscription"
pathNoteSubscribe = "/open-apis/vc/v1/notes/subscription"
pathNoteUnsubscribe = "/open-apis/vc/v1/notes/unsubscription"
pathRecordingSubscribe = "/open-apis/vc/v1/recordings/subscription"
pathRecordingUnsubscribe = "/open-apis/vc/v1/recordings/unsubscription"
pathNoteDetailFmt = "/open-apis/vc/v1/notes/%s"
)
@@ -57,5 +62,53 @@ func Keys() []event.KeyDefinition {
},
RequiredConsoleEvents: []string{eventTypeNoteGenerated},
},
{
Key: eventTypeRecordingStarted,
DisplayName: "Recording started",
Description: "Triggered when a recording_bean recording starts; only generated when connected to Feishu software.",
EventType: eventTypeRecordingStarted,
Schema: event.SchemaDef{
Custom: &event.SchemaSpec{Type: reflect.TypeOf(VCRecordingStartedOutput{})},
},
Process: processVCRecordingStarted,
PreConsume: subscriptionPreConsume(eventTypeRecordingStarted, pathRecordingSubscribe, pathRecordingUnsubscribe),
Scopes: []string{"vc:recording:read"},
AuthTypes: []string{
"user",
},
RequiredConsoleEvents: []string{eventTypeRecordingStarted},
},
{
Key: eventTypeRecordingTranscriptGenerated,
DisplayName: "Recording transcript generated",
Description: "Triggered when recording_bean transcript items are generated; only generated when connected to Feishu software.",
EventType: eventTypeRecordingTranscriptGenerated,
Schema: event.SchemaDef{
Custom: &event.SchemaSpec{Type: reflect.TypeOf(VCRecordingTranscriptGeneratedOutput{})},
},
Process: processVCRecordingTranscriptGenerated,
PreConsume: subscriptionPreConsume(eventTypeRecordingTranscriptGenerated, pathRecordingSubscribe, pathRecordingUnsubscribe),
Scopes: []string{"vc:recording:read"},
AuthTypes: []string{
"user",
},
RequiredConsoleEvents: []string{eventTypeRecordingTranscriptGenerated},
},
{
Key: eventTypeRecordingEnded,
DisplayName: "Recording ended",
Description: "Triggered when a recording_bean recording ends and uploads successfully; only generated when connected to Feishu software.",
EventType: eventTypeRecordingEnded,
Schema: event.SchemaDef{
Custom: &event.SchemaSpec{Type: reflect.TypeOf(VCRecordingEndedOutput{})},
},
Process: processVCRecordingEnded,
PreConsume: subscriptionPreConsume(eventTypeRecordingEnded, pathRecordingSubscribe, pathRecordingUnsubscribe),
Scopes: []string{"vc:recording:read"},
AuthTypes: []string{
"user",
},
RequiredConsoleEvents: []string{eventTypeRecordingEnded},
},
}
}