Files
Cyrene_v2/internal/services/backthink/trigger.go
T
Aska d09b25071d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
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 交叉编译通过
2026-09-08 13:46:43 +08:00

106 lines
3.9 KiB
Go

package backthink
import (
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
"git.yeij.top/AskaEth/Cyrene_v2/internal/iot"
)
// TriggerKind classifies what woke a thinking pass, so the push path can apply the
// right policy: a heartbeat is gate-governed (60min≤1 / cooldown / per-flavor), an
// event is a real signal the owner wants to hear about and bypasses the rate gate.
type TriggerKind int
const (
// TriggerHeartbeat is the periodic autonomous reflection. Its proactive push is
// throttled by the PushGate so the owner is never spammed by empty reflections.
TriggerHeartbeat TriggerKind = iota
// TriggerEvent is a real event (IoT device change, reminder, owner-relevant
// system event). Its push bypasses the PushGate — "该发就发" — subject only to
// same-event dedup.
TriggerEvent
)
// String returns a stable label for logs and records.
func (k TriggerKind) String() string {
switch k {
case TriggerEvent:
return "event"
case TriggerHeartbeat:
return "heartbeat"
default:
return fmt.Sprintf("triggerkind(%d)", int(k))
}
}
// Bus subjects the service subscribes to for event-triggered thinking. They are
// the contract defined by design/12 §1: an IoT state change, a reminder firing,
// or a generic system event.
const (
// SubjectReminderTrigger is the bus subject on which reminders fire.
SubjectReminderTrigger = "reminder.trigger"
// SubjectSystemEvent is the bus subject for generic system events.
SubjectSystemEvent = "system.event"
)
// SystemEvent is the optional typed payload a caller may publish to
// SubjectSystemEvent to convey a system notice. Import defaults to Normal. Under
// design/12 §6 策略一, a low-value system notice (a join/leave ping, generic
// platform noise) is downgraded to a registration-only record and does not wake a
// full thinking pass; only a high-importance (owner-relevant) system event thinks,
// so a busy public platform cannot trigger a flood of expensive reflections.
type SystemEvent struct {
Summary string
Import Importance
}
// Trigger describes what woke a thinking pass: the source subsystem, a
// human-readable summary and the event's estimated importance.
type Trigger struct {
Source string
Summary string
Import Importance
// Kind records whether this pass was the periodic heartbeat or a real event, so
// the push path can bypass the rate gate for a genuine event (owner mandate).
Kind TriggerKind
}
// heartbeatTrigger is the periodic, low-importance trigger. Low importance means
// the pass rarely interrupts the user, honouring "默认不发送 chat 类型消息".
func heartbeatTrigger() Trigger {
return Trigger{Source: "heartbeat", Summary: "periodic autonomous reflection", Import: ImportanceLow, Kind: TriggerHeartbeat}
}
// triggerForEvent maps a bus event to a Trigger. An IoT device change is treated
// as normal importance, a reminder firing as high importance, and a generic
// system event as normal importance. Every event is tagged TriggerEvent so its
// proactive push bypasses the heartbeat PushGate.
func triggerForEvent(e bus.Event) Trigger {
switch e.Subject {
case iot.DeviceUpdateSubject:
if d, ok := e.Payload.(iot.DeviceUpdate); ok {
return Trigger{
Source: e.Subject,
Summary: fmt.Sprintf("device %s (%s) %s", d.Device.ID, d.Device.Type, d.Action),
Import: ImportanceNormal,
Kind: TriggerEvent,
}
}
return Trigger{Source: e.Subject, Summary: e.Subject, Import: ImportanceNormal, Kind: TriggerEvent}
case SubjectReminderTrigger:
return Trigger{Source: e.Subject, Summary: e.Subject, Import: ImportanceHigh, Kind: TriggerEvent}
case SubjectSystemEvent:
tr := Trigger{Source: e.Subject, Summary: e.Subject, Import: ImportanceNormal, Kind: TriggerEvent}
if sy, ok := e.Payload.(SystemEvent); ok {
if sy.Summary != "" {
tr.Summary = sy.Summary
}
tr.Import = sy.Import
}
return tr
default:
return Trigger{Source: e.Subject, Summary: e.Subject, Import: ImportanceNormal, Kind: TriggerEvent}
}
}