feat(channels): 非消息事件接入(notice 归一化 + 传入前审查,fail-safe 静默)与入站脱敏帧日志/日志轮转
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 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 侧改追加(不再重启即截断)
This commit is contained in:
AskaEth committed 2026-10-05 11:55:05 +08:00
1 parent 85abb90067
commit 5139945bfe
15 files changed
+1604 -21

No files matched your search

+201
View File
@@ -0,0 +1,201 @@
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
}