package channelbridge import ( "context" "errors" "fmt" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session" ) // 本文件承载 design/12 §6 策略一补充的「系统通知传入前审查」(pre-entry review): // 系统类事件**先审查、归一化,再决定**是否 / 如何进入她的上下文——裸喂禁止。 // // 分级(结构判定优先,红线:绝不看正文措辞、绝不由模型判身份): // - 可忽略 → 仅登记(不喂 LLM,不驱动 loop); // - 高价值 → 升级真会话(越过「非主人聊天消息」的最小打扰闸门,直接放行); // - 其余 → 提取安全文本、剥离异常字段后**限定注入**(先过既有审查模型剔噪)。 // // 审查失败 / 超时 / 未知 / 解析失败 / 未装配审查模型 → **fail-safe 静默**(见 // noticeFailSafeProceeds)。畸形数据绝不进入 LLM 上下文,也绝不驱动 loop。 const ( // noticeReviewTimeout 是一次系统通知剔噪审查的上限:审查模型卡住绝不能拖住 // 入站流水线(design/12 §6 实现要点的并发条款同款口径)。 noticeReviewTimeout = 5 * time.Second // noticeSystemPrefix 是限定注入时系统行的前缀——它标在 **system 角色**消息上, // 不是用户正文(design/11 §10:用户正文不得塞系统标记)。 noticeSystemPrefix = "[系统通知] " ) // noticeFailSafeProceeds 是 design/12 §6 策略一补充**写死的保守方向**: // false = 审查失败 / 超时 / 未识别 / 解析失败 / 未装配审查模型时**一律静默** // (仅登记,绝不驱动 loop,绝不让畸形数据进入 LLM 上下文)。 // 变异验证就是把它改成 true,看「fail-safe 静默」这组测试变红。 const noticeFailSafeProceeds = false // ErrNoNoticeReviewer 表示未装配审查模型:系统通知的「其余」档没有可用的剔噪器。 // 调用方据此走 fail-safe 静默,绝不因缺模型而放行未审查内容。 var ErrNoNoticeReviewer = errors.New("channelbridge: no system notice reviewer") // noticeOutcome 是传入前审查对一条入站消息的裁决。 type noticeOutcome struct { // Applicable 报告该消息经**结构化元数据**判定为系统事件(已被本审查受理)。 Applicable bool // Proceed 报告归一化后的内容是否允许进入她的上下文。 Proceed bool // Class 是归一化后的结构分类结果。 Class adapter.NoticeClass // Notice 是归一化(且已剥离)后的统一结构。 Notice adapter.SystemNotice // Reason 是简短原因标记,用于日志与排查。 Reason string } // preEntryReview 是「传入前审查」主体:结构化识别 → 归一化 → 剥离 → 分类 → // (其余档)既有审查模型剔噪 → fail-safe。 // // msg 为指针:凡是被判定放行(Proceed)的通知**就地**改写——Content 换成剥离后的 // 安全文本、Media 与 RawData 清空(剥离异常字段:原始载荷、附件一律不进上下文)。 // 这个改写挂在 defer 上,使「放行」只有一条出口:任何分支一旦放行,就绝不留下 // 未剥离的原始字段(反之任何静默分支都不改写任何东西)。 func (b *Bridge) preEntryReview(ctx context.Context, msg *adapter.UnifiedMessage, a adapter.PlatformAdapter) (out noticeOutcome) { defer func() { if !out.Proceed || out.Notice.SafeText == "" { return } msg.Content = out.Notice.SafeText msg.Media = nil msg.RawData = nil }() if !isSystemNoticeMessage(*msg) { return noticeOutcome{} } norm, ok := systemNoticeNormalizer(a) if !ok { return noticeOutcome{Applicable: true, Proceed: noticeFailSafeProceeds, Reason: "system event without a normalizer for this adapter"} } notice, ok := normalizeNoticeSafely(norm, *msg) if !ok { return noticeOutcome{Applicable: true, Proceed: noticeFailSafeProceeds, Reason: "system notice unparsable"} } notice.SafeText = adapter.SanitizeNoticeText(notice.SafeText) class := notice.Classify() out = noticeOutcome{Applicable: true, Class: class, Notice: notice} if notice.SafeText == "" { out.Reason = "system notice carries no safe text" out.Proceed = noticeFailSafeProceeds return out } switch class { case adapter.NoticeRegisterOnly: out.Reason = "ignorable system event" return out case adapter.NoticeEscalate: out.Proceed = true out.Reason = "high-value system event escalated to a real session" default: keep, err := b.reviewNoticeNoise(ctx, *msg, notice) if err != nil { out.Reason = "notice review failed: " + judgeReason(err.Error()) out.Proceed = noticeFailSafeProceeds return out } if !keep { // 这一侧不是 fail-safe:是审查模型明确判「没必要」。静默是它的裁决本身, // 与审查失败那条路径(noticeFailSafeProceeds)无关。 out.Reason = "notice review dropped the event as noise" return out } out.Proceed = true out.Reason = "notice review kept the event" } return out } // reviewNoticeNoise 复用 design/12 §6 策略四的**同一个**审查模型注入点 // (Options.Review / adapter.Reviewer,绝不另造第二套)对一条无法结构归类的系统 // 通知做一次成本受控的剔噪:返回 true 表示值得进上下文。 // 它把单条消息当作长度为 1 的队列交给审查器,因此复用其「保留编号」契约与 // 越界/重复索引的防幻觉处理。审查模型只看剥离后的安全文本,绝不见原始载荷; // 出错(含超时)一律往上抛,由调用方走 fail-safe 静默。 func (b *Bridge) reviewNoticeNoise(ctx context.Context, msg adapter.UnifiedMessage, notice adapter.SystemNotice) (bool, error) { if b.review == nil { return false, ErrNoNoticeReviewer } probe := msg probe.Content = notice.SafeText probe.Media = nil probe.RawData = nil reviewCtx, cancel := context.WithTimeout(ctx, noticeReviewTimeout) defer cancel() keep, err := b.review(reviewCtx, []adapter.UnifiedMessage{probe}) if err != nil { return false, fmt.Errorf("channelbridge: notice review: %w", err) } for _, i := range keep { if i == 0 { return true, nil } } return false, nil } // registerNoticeOnly 是审查判「仅登记 / fail-safe 静默」那一侧的收口:把安全文本写进 // 该消息所属会话的平台日志(登记),**绝不驱动 loop、绝不调用 LLM**。它与最小打扰 // 路径共用 registerSilenced,因此「登记」只有一条实现。 func (b *Bridge) registerNoticeOnly(ctx context.Context, msg adapter.UnifiedMessage, a adapter.PlatformAdapter, out noticeOutcome) { if out.Notice.SafeText == "" { b.noticeLog.Debug("system notice registered without text", "platform", msg.Platform, "channel", msg.ChannelID, "reason", out.Reason) return } msg.Content = out.Notice.SafeText actor := session.ResolveActor(msg, b.owner) sess, err := b.sessions.GetOrCreate(ctx, msg, b.owner) if err != nil { b.noticeLog.Warn("system notice session resolve failed, not registered", "platform", msg.Platform, "channel", msg.ChannelID, "error", err) return } b.registerSilenced(ctx, msg, actor, sess, a) b.noticeLog.Debug("system notice registered only (no loop)", "platform", msg.Platform, "channel", msg.ChannelID, "kind", out.Notice.Kind, "reason", out.Reason) } // isSystemNoticeMessage 只按**结构化元数据**判定一条消息是否是系统类事件: // 适配器在归一化时把它标成 ContentAction(非聊天动作信号:进群/退群/撤回/戳一戳/ // 系统提醒)。它**绝不看正文**,也绝不用提示词判断(项目红线)。 func isSystemNoticeMessage(msg adapter.UnifiedMessage) bool { return msg.ContentType == adapter.ContentAction } // systemNoticeNormalizer 取适配器的归一化 seam。未实现该可选接口的适配器返回 // ok=false——调用方据此走 fail-safe 静默(宁可登记,也不放行看不懂的平台事件)。 func systemNoticeNormalizer(a adapter.PlatformAdapter) (adapter.SystemNoticeNormalizer, bool) { if a == nil { return nil, false } norm, ok := a.(adapter.SystemNoticeNormalizer) if !ok || norm == nil { return nil, false } return norm, true } // normalizeNoticeSafely 调用适配器的归一化 seam,并把任何 panic 折成 ok=false。 // 各平台的系统通知格式多变且不确定,畸形载荷把某个解析器打崩**不能**牵连常驻后端 // ——按 design/12 §6 策略一补充的审查钩子,这一侧同样是 fail-safe 静默。 func normalizeNoticeSafely(norm adapter.SystemNoticeNormalizer, msg adapter.UnifiedMessage) (notice adapter.SystemNotice, ok bool) { defer func() { if r := recover(); r != nil { notice, ok = adapter.SystemNotice{}, false } }() notice, ok = norm.NormalizeSystemNotice(msg) return notice, ok }