- notice:adapter 归一化 seam + 结构化分类 + 净化(ANSI/控制/零宽/双向字符、200 runes 截断) + fail-safe 写死为「静默」(未装配审查/超时/失败 ⇒ 仅登记、不驱动 loop) - 身份面零改动:notice 发送者恒 RoleSystem + SenderID=system - 复用策略四的同一个审查模型(Options.Review),未另造第二套 - 入站帧脱敏 DEBUG(post_type/message_type/sub_type/self_id/user_id/sender_id/group_id/段类型/at 目标) - ctrl.bat 启动前归档轮转(保留 20 份)+ bash 侧改追加(不再重启即截断)
202 lines
8.7 KiB
Go
202 lines
8.7 KiB
Go
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: " + 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
|
||
}
|