Files
Cyrene_v2/internal/services/backthink/trigger.go
T
AskaEth 213aff2e33
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(backthink): 存档落「紧急度」精确档位(裁决 14 的精确化前置)
- backthink_records 自建表加 urgency TEXT NOT NULL DEFAULT ''(**不开迁移号、不动 internal/db**;对既有库幂等补列:PRAGMA 判存在 + 容忍 duplicate column,其它错误照报)
- 落库值 = 该轮**实际生效**的档位(escalatedImport 折叠后的 roundImport;她自报可上抬、事件自带高档不被她压低)——「生效值优先」而非「她的话优先」,避免与既有 escalateImport 口径打架
- Record.Urgency 的 JSON 键刻意是 urgency ⇒ WS 实时帧自然带上、前端防御性读取即刻显示(不必等重开页面)
- 思考报错行 / 未唤醒降级行 / 历史旧行 = **空串(未知)**,绝不反推(变异:【空串当 routine】⇒ 4 条测试红;【补列非幂等】⇒ 41 条红,新库启动即失败)
- 生产影响:下次启动对真实 data/cyrene.db 执行一次单列 ALTER,并记 source=services/backthink 的 Info「backthink records table upgraded」
2026-10-05 14:10:25 +08:00

188 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package backthink
import (
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
"git.yeij.top/AskaEth/Cyrene_v2/internal/iot"
"git.yeij.top/AskaEth/Cyrene_v2/internal/urgency"
)
// TriggerKind 归类唤醒一轮思考的原因,好让推送路径套用
// 正确的策略:心跳受门控管辖(60min≤1 / 冷却 / 按风格),
// 事件是主人确实想听到的真实信号、绕过频率门。
type TriggerKind int
const (
// TriggerHeartbeat 是周期性自主反思。它的主动推送受
// PushGate 限流,使主人绝不会被空洞的反思刷屏。
TriggerHeartbeat TriggerKind = iota
// TriggerEvent 是真实事件(IoT 设备变化、提醒、与主人相关的
// 系统事件)。它的推送绕过 PushGate——"该发就发"——只受
// 同一事件去重约束。
TriggerEvent
)
// String 返回用于日志与记录的稳定标签。
func (k TriggerKind) String() string {
switch k {
case TriggerEvent:
return "event"
case TriggerHeartbeat:
return "heartbeat"
default:
return fmt.Sprintf("triggerkind(%d)", int(k))
}
}
// 本服务为事件驱动思考订阅的总线主题。它们是
// design/12 §1 定义的契约:一次 IoT 状态变化、一次提醒触发,
// 或一次泛化的系统事件。
const (
// SubjectReminderTrigger 是提醒触发所用的总线主题。
SubjectReminderTrigger = "reminder.trigger"
// SubjectSystemEvent 是泛化系统事件的总线主题。
SubjectSystemEvent = "system.event"
)
// SystemEvent 是调用方可发布到 SubjectSystemEvent 以传达
// 系统通知的可选类型化负载。Import 默认为 Normal。在
// design/12 §6 策略一之下,低价值的系统通知(加入/退出 ping、泛化的
// 平台噪声)被降级为仅登记一条记录,不唤醒
// 一整轮思考;只有高重要度(与主人相关)的系统事件才思考,
// 因此繁忙的公开平台无法触发昂贵反思的洪流。
//
// 它是「载荷自带重要度」这条通用通道的既有形式;生产者的紧急度若以
// urgency.Declarer 声明(IoT 的设备跃迁即如此),由 triggerForEvent 折算到
// 同一个 Trigger.Import 字段上,两条形式因此落到同一套判定。
type SystemEvent struct {
Summary string
Import Importance
}
// Trigger 描述唤醒一轮思考的原因:来源子系统、
// 可读摘要与事件的估计重要度。
type Trigger struct {
Source string
Summary string
Import Importance
// Kind 记录本轮是周期性心跳还是真实事件,好让
// 推送路径能为真实事件绕过频率门(主人要求)。
Kind TriggerKind
}
// heartbeatTrigger 是周期性、低重要度的触发。低重要度意味着
// 该轮很少打扰用户,遵守 "默认不发送 chat 类型消息"。
func heartbeatTrigger() Trigger {
return Trigger{Source: "heartbeat", Summary: "periodic autonomous reflection", Import: ImportanceLow, Kind: TriggerHeartbeat}
}
// StartupSummary 是启动轮思考(裁决 07.4.4,design/12 §1)的触发摘要。它同时是
// context.go 里切换「拟人化启动消息」专属引导提示词的标识。
const StartupSummary = "process startup"
// startupTrigger 是进程启动后的第一轮思考(裁决 07.4.4):不等满一个心跳周期,
// 启动立即触发,并引导她以角色口吻产出一条拟人化启动问候(如「我回来啦」)。
// Kind 仍为 TriggerHeartbeat:启动轮的推送与心跳走完全相同的门与限频
// (PushGate / 打扰级联 / 活动门),进程反复重启也不会刷屏主人。
func startupTrigger() Trigger {
return Trigger{Source: "startup", Summary: StartupSummary, Import: ImportanceLow, Kind: TriggerHeartbeat}
}
// triggerForEvent 把总线事件映射为 Trigger:主题给出可读摘要与主题级默认重要度,
// 随后**通用通道**统一解出重要度 —— 任何事件载荷只要实现 urgency.Declarer
// (IoT 的 DeviceUpdate 是第一个使用者),它声明的紧急度就被承认;泛化系统事件沿用
// 它既有的 SystemEvent.Import 口径。两条形式最终都落在同一个 Trigger.Import 字段上,
// 判定侧(decide.go)因此只认档位、不认生产者。每个事件都打上 TriggerEvent,
// 因此其主动推送绕过心跳的 PushGate。
func triggerForEvent(e bus.Event) Trigger {
tr := triggerForSubject(e)
if d, ok := e.Payload.(urgency.Declarer); ok {
tr.Import = escalateImport(tr.Import, d.Urgency())
}
return tr
}
// triggerForSubject 给出各主题的可读摘要与主题级默认重要度(未声明紧急度的事件就
// 停在这一档)。它刻意不对任何生产者做专有判定:重要度已由通用通道统一解出。
func triggerForSubject(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}
}
}
// importanceForUrgency 把生产者声明的紧急度折算为思考轮的重要度。它是通用通道与
// 判定侧之间**唯一**的换算点(design/12 §6 安全域豁免,裁决 1.8 / 07.4.2):零值
// Routine 折算 Normal —— 默认档永不获得穿透安静时段的资格;只有生产者显式声明
// critical 的安全域事件拿到 ImportanceCritical,从而穿透安静时段与工作 / 忙碌。
func importanceForUrgency(l urgency.Level) Importance {
switch l {
case urgency.LevelCritical:
return ImportanceCritical
case urgency.LevelWarning:
return ImportanceHigh
default:
return ImportanceNormal
}
}
// urgencyForImportance 是 importanceForUrgency 的**反向读法**:把一轮思考**实际生效**
// 的重要度还原成通用紧急度词汇,供存档携带精确档位(X2a;面板此前只能给出「下限」)。
//
// 它复用同一张既有对应关系(Critical↔critical、High↔warning),不引入任何新的门限或
// 折算规则:例行档对应 importanceForUrgency 的 Normal,而 ImportanceLow 只是「比例行
// 还安静」的心跳/启动轮 —— urgency 词汇里没有更低的档,故与 Normal 同归 routine
// (无损:routine 本就表示「安静时段照常扣下」这一类)。按档位高低比较而不是逐值
// 相等,是让将来插入更高档时它落在最高档、而不是静默掉回 routine。
//
// 取数点在 backthink.go 的 think:入参是该轮**实际生效**的重要度
// (escalatedImport 的结果,她自报的紧急度已折叠在内)。
func urgencyForImportance(i Importance) urgency.Level {
switch {
case i >= ImportanceCritical:
return urgency.LevelCritical
case i >= ImportanceHigh:
return urgency.LevelWarning
default:
return urgency.LevelRoutine
}
}
// escalateImport 把声明的紧急度叠到基础重要度上。声明只能**上抬**、绝不压低,因此
// 主题级默认(如提醒的 High)与安全域事件既得的档位不会被一句 routine 抹掉;
// 未声明(零值 LevelRoutine)时原样返回 base,没标紧急度的事件行为逐字不变。
//
// 两个调用方共用它:总线事件的载荷声明(triggerForEvent),以及她自己在主动思考轮
// 结论里声明「我认为这条紧急」(backthink.go 的 escalatedImport)。
func escalateImport(base Importance, declared urgency.Level) Importance {
if declared == urgency.LevelRoutine {
return base
}
if up := importanceForUrgency(declared); up > base {
return up
}
return base
}