feat(backthink): 事件驱动主动消息绕过PushGate——IoT等事件该发就发(heartbeat仍受60min节流)
- trigger.go: 新增 TriggerKind(TriggerHeartbeat/TriggerEvent) + Trigger.Kind 标记触发来源 - push.go: PushRequest.TriggerKind 分流——event 跳过 gate.Allow/RecordPush(该发就发),heartbeat 仍受 60min≤1/冷却/分类限频;PushResult.Bypassed 标记 + detail=event_bypass 区分 - backthink.go: think() 传 TriggerKind + 过同事件去重门 - event_dedup.go(新): 同事件(Source+Summary)去重防刷,无 60min 节流;不同事件/心跳后重置链 - 测试: event 连续 3 次全推 bypass / heartbeat 第2次 rate_limited / event 不消耗心跳限 / 同事件去重 - build/vet/test 全绿 + windows/darwin 交叉编译通过
This commit is contained in:
1 parent
3e4efa0e41
commit
d09b25071d
6 files changed
+266
-22
No files matched your search
@@ -50,6 +50,10 @@ type PushResult struct {
|
||||
Pushed bool
|
||||
// Route is the delivery route taken.
|
||||
Route Route
|
||||
// Bypassed reports whether this push was event-driven and skipped the PushGate
|
||||
// (a real event, "该发就发"). A heartbeat push is gate-governed and never
|
||||
// bypassed.
|
||||
Bypassed bool
|
||||
// Detail is a short human-readable note (e.g. client count, decision reason).
|
||||
Detail string
|
||||
// Err is the underlying error text when delivery failed, or that of the
|
||||
@@ -60,9 +64,9 @@ type PushResult struct {
|
||||
// String returns a concise summary for logs.
|
||||
func (p PushResult) String() string {
|
||||
if p.Err != "" {
|
||||
return fmt.Sprintf("pushed=%v route=%s detail=%q err=%q", p.Pushed, p.Route, p.Detail, p.Err)
|
||||
return fmt.Sprintf("pushed=%v route=%s bypass=%v detail=%q err=%q", p.Pushed, p.Route, p.Bypassed, p.Detail, p.Err)
|
||||
}
|
||||
return fmt.Sprintf("pushed=%v route=%s detail=%q", p.Pushed, p.Route, p.Detail)
|
||||
return fmt.Sprintf("pushed=%v route=%s bypass=%v detail=%q", p.Pushed, p.Route, p.Bypassed, p.Detail)
|
||||
}
|
||||
|
||||
// ActiveClient delivers a proactive message to every currently active web/native
|
||||
@@ -106,6 +110,9 @@ type PushRequest struct {
|
||||
Message adapter.UnifiedMessage
|
||||
// ChannelHint is the requested target channel, "" when she did not choose one.
|
||||
ChannelHint string
|
||||
// TriggerKind records what woke this pass. An event-driven push bypasses the
|
||||
// PushGate (owner mandate: "该发就发"); a heartbeat stays gate-governed.
|
||||
TriggerKind TriggerKind
|
||||
}
|
||||
|
||||
// Pusher delivers a proactive message to ONE target channel. It is bounded by the
|
||||
@@ -148,7 +155,12 @@ func (p *pusher) Push(ctx context.Context, req PushRequest) PushResult {
|
||||
return PushResult{Pushed: false, Route: RouteNone, Err: err.Error()}
|
||||
}
|
||||
kind := classifyPush(req.Message.Content)
|
||||
if p.gate != nil && !p.gate.Allow(ctx, kind) {
|
||||
// A real event-driven push bypasses the PushGate: it should go out as-is ("该发就发"),
|
||||
// independent of the heartbeat throttle (60min≤1 / cooldown / per-flavor window).
|
||||
// Only the periodic heartbeat stays gate-governed, so the owner is never spammed
|
||||
// by an empty reflection. Same-event dedup is applied upstream in think().
|
||||
eventDriven := req.TriggerKind == TriggerEvent
|
||||
if p.gate != nil && !eventDriven && !p.gate.Allow(ctx, kind) {
|
||||
p.log.Warn("backthink push rate limited", "kind", kind.String())
|
||||
return PushResult{Pushed: false, Route: RouteRateLimited, Detail: "proactive push rate limited"}
|
||||
}
|
||||
@@ -174,12 +186,18 @@ func (p *pusher) Push(ctx context.Context, req PushRequest) PushResult {
|
||||
res = p.pushActive(ctx, msg)
|
||||
}
|
||||
}
|
||||
// Only a real delivery advances the cooldown/per-kind windows, so an
|
||||
// attempted-but-failed push does not suppress a later genuine one.
|
||||
if res.Pushed && p.gate != nil {
|
||||
// Only a real delivery advances the cooldown/per-kind windows (and only for a
|
||||
// gate-governed push), so an attempted-but-failed push does not suppress a later
|
||||
// genuine one, and an event push never contributes to the heartbeat throttle.
|
||||
if res.Pushed && p.gate != nil && !eventDriven {
|
||||
p.gate.RecordPush(kind)
|
||||
}
|
||||
return annotateHint(res, req.ChannelHint)
|
||||
res = annotateHint(res, req.ChannelHint)
|
||||
if eventDriven {
|
||||
res.Bypassed = true
|
||||
res = annotateBypass(res)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// pushActive delivers only to the active web/native client.
|
||||
@@ -246,6 +264,17 @@ func annotateHint(res PushResult, hint string) PushResult {
|
||||
return res
|
||||
}
|
||||
|
||||
// annotateBypass tags an event-driven push result so the record/panel shows it
|
||||
// skipped the PushGate, distinct from a heartbeat push that was rate-limited.
|
||||
func annotateBypass(res PushResult) PushResult {
|
||||
if res.Detail == "" {
|
||||
res.Detail = "event_bypass"
|
||||
} else {
|
||||
res.Detail = res.Detail + ", event_bypass"
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// rateGate is the production PushGate: a sliding-window counter over
|
||||
// session.RateLimiter (design/11 §6 anti-spam rule) combined with a post-push
|
||||
// cooldown and a per-flavor (问候/提醒/分享/关心) window so the owner is never
|
||||
|
||||
Reference in new issue
Block a user