Files
Cyrene_v2/internal/services/channelbridge/proactive_notify.go
T
AskaEth f4adf0b526
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(notify): IM 路由的主动消息也在非前台补浏览器通知(契约 v0.74)
主人裁定 C 的必然推论:她该不该提醒我,与后端走了哪条路由无关。P0 如实报回的缺口 = 通知只挂在 proactive_message 帧上,而 auto 提示在有已连接 IM 渠道时优先走 IM ⇒ 最常发生的路径恰恰不弹。

判据(先查现状、不猜):行字段**区分不出**主动推送与普通回复(已用真库证据排除 platform/adapterId/turnId 反推)⇒ 新增**结构化的正向声明**:帧顶 data.proactive === true,只由主动推送那条通道打(gateway.BroadcastProactivePersisted);普通路径永远传 false。
- 后端:channelbridge 抽出唯一写入口径 persistMain + 新增 proactive_notify 接缝;gateway 抽出 broadcastPersisted(msg, proactive) 两入口共用
- 前端:handlers/proactive-notice.ts 两条帧路共用同一套提醒运行时(有界去重 + 降级说明 + DOM/Notification 取值);message.ts 读到判据才走提醒
- 对照断言(必须有):普通 IM 回复/主人提问/显式 proactive:false ⇒ **0 通知、3 气泡**(不误伤)
- 变异实测:去掉非主动保护 / 跨两路去重失效 / 前台也弹 / IM 不发判据帧 / 普通广播带判据 ⇒ 5 条全部打红
- 契约 v0.74(纯增量:message_persisted 新增可选 proactive:true)
- 边界(如实):判据**不落库**(翻历史不弹提醒,也不得从行字段反推);落库失败时不发判据帧(无权威 id 的帧前端整帧丢弃)
2026-10-05 19:47:54 +08:00

41 lines
2.2 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 channelbridge
import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
)
// 本文件承载 IM 主动推送那条**实时通知**接缝(切片 P0b「IM 路由的主动消息也要在非前台
// 补通知」)。它与 persist.go 的 notifyPersisted 并列:普通消息走那一条,主动推送走这
// 一条。两条都写主会话,因此「写」是同一份代码(persistMain),差别只在通知。
//
// 为什么必须分开而不是去猜:主人裁定「她说话该不该提醒我,与走哪条路由无关」,因此
// 主动推送必须自己声明「我是主动推送」。判据是**结构化**的(本次写入走的是哪条通知
// 通道),**绝不**读正文关键词、**绝不**让提示词/模型判断 —— 后者是项目红线
// (AGENTS §9:身份与判定靠元数据,不靠提示词)。
//
// 为什么不在落库行上找判据:主会话的 `platform`/`adapter_id` 区分不出主动推送与普通
// 回复 —— 主动推送写的是**解析后的真实平台**(proactive.go 的 target.platform,例如
// "weixin-ilink"),与普通回复行同一形状(普通回复在按平台名注册的适配器上
// adapter_id 也等于平台名)。因此结构化判据放在**落库那一刻的通知通道**上,随帧顶的
// `proactive:true` 到达客户端(见 gateway 的 BroadcastProactivePersisted)。
// SetOnProactivePersisted 装配(或替换)「一条主动消息刚落进主会话」之后所调用的通知
// 钩子。组合根把它接进 gateway 的 BroadcastProactivePersisted(帧顶带结构化判据的
// `message_persisted`)。它可以安全空调用,也可与在途推送安全并发。
func (b *Bridge) SetOnProactivePersisted(fn func(history.Message)) {
b.mu.Lock()
defer b.mu.Unlock()
b.onProactivePersisted = fn
}
// notifyProactive 在装配了主动通知钩子时把刚落库的主动消息交给它;未装配时是空操作
// (行为与本切片之前逐字相同:主动消息照旧落库、只是没有「主动」这条通知通道)。
func (b *Bridge) notifyProactive(msg history.Message) {
b.mu.RLock()
fn := b.onProactivePersisted
b.mu.RUnlock()
if fn != nil {
fn(msg)
}
}