feat: integrate creator hub douyin workflows

This commit is contained in:
2026-09-14 19:22:07 +08:00
parent 025fe62c37
commit 44a28954cf
42 changed files with 2850 additions and 880 deletions
+96 -4
View File
@@ -34,6 +34,7 @@ type creatorGatewayEventNotice struct {
InteractorUID string `json:"interactor_uid"`
CommentID string `json:"comment_id"`
WorkID string `json:"work_id"`
MessageType string `json:"message_type,omitempty"`
MessageText string `json:"message_text,omitempty"`
PlatformEventAt string `json:"platform_event_at,omitempty"`
GatewayReceivedAt string `json:"gateway_received_at,omitempty"`
@@ -46,6 +47,68 @@ type creatorEventBinding struct {
gateway hub.Gateway
}
type creatorUpdateHub struct {
mu sync.Mutex
subscribers map[chan struct{}]struct{}
}
var creatorUpdates = &creatorUpdateHub{subscribers: make(map[chan struct{}]struct{})}
func (h *creatorUpdateHub) subscribe() (<-chan struct{}, func()) {
channel := make(chan struct{}, 1)
h.mu.Lock()
h.subscribers[channel] = struct{}{}
h.mu.Unlock()
return channel, func() {
h.mu.Lock()
if _, ok := h.subscribers[channel]; ok {
delete(h.subscribers, channel)
close(channel)
}
h.mu.Unlock()
}
}
func (h *creatorUpdateHub) publish() {
h.mu.Lock()
defer h.mu.Unlock()
for channel := range h.subscribers {
select {
case channel <- struct{}{}:
default:
}
}
}
func creatorListenerGeneration(env hub.EnvironmentContext) string {
return fmt.Sprintf("%s:%s:%d", env.RuntimeID, env.RuntimeNetworkID, env.BindingVersion)
}
func listenerBoundaryPointer(value time.Time) *time.Time {
if value.IsZero() {
return nil
}
value = value.UTC()
return &value
}
func persistCreatorListenerState(ctx context.Context, store *creator.Store, binding creatorEventBinding, status, reason string, boundaryAt *time.Time, deliveryID string) {
if store == nil {
return
}
if _, err := store.UpsertListenerState(ctx, creator.ListenerState{
AccountID: binding.accountID,
Platform: creator.PlatformDouyin,
Generation: creatorListenerGeneration(binding.env),
Status: status,
BoundaryAt: boundaryAt,
LastDeliveryID: deliveryID,
Reason: reason,
}); err != nil {
logrus.WithError(err).WithField("account_id", binding.accountID).Warn("creator listener state persistence failed")
}
}
func (binding creatorEventBinding) key() string {
return fmt.Sprintf("%s\x00%s\x00%s\x00%d\x00%s\x00%s\x00%s\x00%s", binding.gateway.Name, binding.gateway.Endpoint, binding.gateway.Token, binding.env.BindingVersion, binding.env.RuntimeID, binding.env.RuntimeNetworkID, binding.env.Exit.ID, binding.uid)
}
@@ -180,7 +243,11 @@ func runCreatorEventListener(ctx context.Context, store *creator.Store, binding
startPayload[key] = value
}
startPayload["expected_uid"] = binding.uid
defer stopCreatorEventListener(binding.accountID, binding.gateway, path, generation)
persistCreatorListenerState(ctx, store, binding, "starting", "等待平台边界标记", nil, "")
defer func() {
stopCreatorEventListener(binding.accountID, binding.gateway, path, generation)
persistCreatorListenerState(context.WithoutCancel(ctx), store, binding, "stopped", "监听已停止", nil, "")
}()
backoff := time.Second
for ctx.Err() == nil {
@@ -190,6 +257,7 @@ func runCreatorEventListener(ctx context.Context, store *creator.Store, binding
err = fmt.Errorf("gateway returned HTTP %d", status)
}
logrus.WithError(err).WithField("account_id", binding.accountID).Warn("creator event listener start failed")
persistCreatorListenerState(ctx, store, binding, "error", err.Error(), nil, "")
if !waitCreatorEventBackoff(ctx, backoff) {
return
}
@@ -211,11 +279,13 @@ func runCreatorEventListener(ctx context.Context, store *creator.Store, binding
err = fmt.Errorf("gateway returned HTTP %d", status)
}
logrus.WithError(err).WithField("account_id", binding.accountID).Warn("creator event listener poll failed")
persistCreatorListenerState(ctx, store, binding, "gap", err.Error(), listenerBoundaryPointer(boundaryAt), "")
break
}
var events []creatorGatewayEvent
if err := json.Unmarshal(body, &events); err != nil {
logrus.WithError(err).WithField("account_id", binding.accountID).Warn("creator event listener response is invalid")
persistCreatorListenerState(ctx, store, binding, "gap", "监听响应无法解析: "+err.Error(), listenerBoundaryPointer(boundaryAt), "")
break
}
for _, event := range events {
@@ -236,10 +306,16 @@ func runCreatorEventListener(ctx context.Context, store *creator.Store, binding
ready = false
boundaryAt = time.Time{}
}
status, reason := "gap", "平台边界无效"
if ready {
status, reason = "ready", ""
}
persistCreatorListenerState(ctx, store, binding, status, reason, listenerBoundaryPointer(boundaryAt), event.DeliveryID)
} else if event.Kind == "open" || event.Kind == "error" || event.Kind == "close" || event.Kind == "reconnected" {
// A transport event never proves continuity. Only the explicit
// boundary marker permits automatic writes again.
ready = false
persistCreatorListenerState(ctx, store, binding, "gap", event.Reason, listenerBoundaryPointer(boundaryAt), event.DeliveryID)
}
if event.Kind == "notice" {
if needsBaseline, reason := creatorGatewayEventNeedsBaseline(event, ready); needsBaseline {
@@ -249,6 +325,12 @@ func runCreatorEventListener(ctx context.Context, store *creator.Store, binding
event.Baseline = true
event.Reason = "平台事件早于监听边界"
}
status := "gap"
reason := event.Reason
if ready && !event.Baseline {
status, reason = "ready", ""
}
persistCreatorListenerState(ctx, store, binding, status, reason, listenerBoundaryPointer(boundaryAt), event.DeliveryID)
}
handleCreatorGatewayEvent(ctx, store, binding, event, executor, generator)
}
@@ -360,6 +442,7 @@ func handleCreatorGatewayEvent(ctx context.Context, store *creator.Store, bindin
logrus.WithError(err).WithFields(logrus.Fields{"account_id": binding.accountID, "event_key": input.EventKey}).Warn("creator event receipt failed")
return
}
creatorUpdates.publish()
if input.EventType == "dm" && input.InteractorUID != "" {
messageAt := input.PlatformEventAt
if messageAt == nil {
@@ -369,7 +452,7 @@ func handleCreatorGatewayEvent(ctx context.Context, store *creator.Store, bindin
}
messageAt = &receivedAt
}
if _, _, messageErr := store.SaveMessage(ctx, creator.MessageInput{Platform: input.Platform, AccountID: input.ReceivingAccountID, PeerUID: input.InteractorUID, PlatformMessageKey: "event:" + input.EventKey, Direction: "inbound", MessageType: "text", Text: input.MessageText, SentState: "received", MessageAt: messageAt}); messageErr != nil {
if _, _, messageErr := store.SaveMessage(ctx, creator.MessageInput{Platform: input.Platform, AccountID: input.ReceivingAccountID, PeerUID: input.InteractorUID, PlatformMessageKey: "event:" + input.EventKey, Direction: "inbound", MessageType: input.MessageType, Text: input.MessageText, SentState: "received", MessageAt: messageAt}); messageErr != nil {
logrus.WithError(messageErr).WithFields(logrus.Fields{"account_id": binding.accountID, "event_key": input.EventKey}).Warn("creator direct message persistence failed")
return
}
@@ -387,6 +470,7 @@ func handleCreatorGatewayEvent(ctx context.Context, store *creator.Store, bindin
return
}
logrus.WithFields(logrus.Fields{"account_id": binding.accountID, "event_key": input.EventKey, "event_type": input.EventType, "state": result.Event.State}).Info("creator event processed")
creatorUpdates.publish()
}()
}
@@ -424,7 +508,14 @@ func creatorEventFromGatewayNotice(accountID string, notice creatorGatewayEventN
if (notice.CommentID != "" && !creatorEventID(notice.CommentID)) || (notice.WorkID != "" && !creatorEventID(notice.WorkID)) {
return creator.InteractionEvent{}, creator.ErrInvalid
}
result := creator.InteractionEvent{Platform: creator.PlatformDouyin, ReceivingAccountID: accountID, EventKey: notice.EventKey, EventType: notice.EventType, InteractorUID: notice.InteractorUID, CommentID: notice.CommentID, WorkID: notice.WorkID, MessageText: strings.TrimSpace(notice.MessageText)}
messageType := strings.TrimSpace(notice.MessageType)
if messageType == "" {
messageType = creator.MessageTypeText
}
if !creator.ValidMessageType(messageType) {
return creator.InteractionEvent{}, creator.ErrInvalid
}
result := creator.InteractionEvent{Platform: creator.PlatformDouyin, ReceivingAccountID: accountID, EventKey: notice.EventKey, EventType: notice.EventType, InteractorUID: notice.InteractorUID, CommentID: notice.CommentID, WorkID: notice.WorkID, MessageType: messageType, MessageText: strings.TrimSpace(notice.MessageText)}
if strings.TrimSpace(notice.PlatformEventAt) != "" {
at, err := time.Parse(time.RFC3339Nano, notice.PlatformEventAt)
if err != nil {
@@ -438,7 +529,8 @@ func creatorEventFromGatewayNotice(accountID string, notice creatorGatewayEventN
if err != nil {
return creator.InteractionEvent{}, fmt.Errorf("invalid gateway receipt time: %w", err)
}
result.ReceivedAt = receivedAt.UTC()
receivedAt = receivedAt.UTC()
result.GatewayReceivedAt = &receivedAt
}
return result, nil
}