feat(channels): 陌生人私聊正常聊天(跳概率门)+ 强制流控 6条/分与回合槽位 + 安静时段不再硬静默 + 受限车道边界行前置
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 陌生人私聊:不再走「值不值得回」的概率门,唯一闸门 = 每发送者 6 条/分滑窗 + 同会话 1 个在跑回合
- 被限流只登记不回,且留带 source 的可见告警(不静默)
- 安静时段在回复侧只作信号(规则层不再静默),群聊取消夜间静默
- 锁定流控常量:FlowDirectDefault 加字面量钉桩测试(放大它 = 静默关流控,且会让额度循环跑百万轮)
This commit is contained in:
AskaEth committed 2026-10-05 13:26:15 +08:00
1 parent c6f5c3fea0
commit 7ae4298871
10 files changed
+654 -118

No files matched your search

+65 -33
View File
@@ -11,15 +11,17 @@ import (
)
// 本文件承载渠道桥接器面向公开平台的入站「最小打扰」分级
// (DevDocs/design/12 §6 策略一/二/五)。非主人的群聊/陌生人
// 消息在进入完整 agent 循环前先经廉价判断,使繁忙的
// 公开群不会把每条消息都扔进昂贵的 LLM 轮次。这些逻辑
// 放在这里是为了让 channelbridge.go 保持在模块体积上限内;它绝不
// 读取消息内容来判定身份——身份早已由
// (DevDocs/design/12 §6 策略一/二)。非主人的**群聊**消息在进入完整
// agent 循环前先经廉价判断,使繁忙的公开群不会把每条消息都扔进
// 昂贵的 LLM 轮次;**陌生人私聊**则一律正常聊天(主人 2026-10-05 裁决①),
// 只受强制流控约束(裁决③)。这些逻辑放在这里是为了让 channelbridge.go
// 保持在模块体积上限内;它绝不读取消息内容来判定身份——身份早已由
// 会话/actor 层结构化解析完毕(项目红线)。
// 安静时段默认值,与 backthink 服务保持一致,使桥接器与
// 后台思考器共用同一个免打扰窗口(design/12 §6 策略五)。
// 后台思考器共用同一个免打扰窗口(design/12 §6 策略五)。它在本车道里
// **只作为信号**随 inQuiet 传给参与度引擎/轻量判定器,不再让规则层静默
// (主人 2026-10-05 裁决②)。
const (
defaultQuietStartHour = 23
defaultQuietEndHour = 7
@@ -32,11 +34,10 @@ type lightVerdict int
const (
// verdictRespond 表示规则信号显示该消息值得回复。
verdictRespond lightVerdict = iota
// verdictSilent 表示规则信号显示该消息应登记但不应
// 回复(例如安静时段内没有强信号)。
verdictSilent
// verdictInconclusive 表示规则无法判断;交给 LightJudge(未装配
// 时则失败关闭为静默)。
// verdictInconclusive 表示规则无法判断;交给参与度引擎(design/18)
// 或注入式 LightJudge(未装配时则失败关闭为静默)。安静时段**不再**
// 单独构成静默(主人 2026-10-05 裁决②)——它只作为信号随 inQuiet
// 传给下游判定器,让轻思考/参与度自己权衡要不要回。
verdictInconclusive
)
@@ -130,12 +131,14 @@ func hasHighValueKeyword(text string) bool {
}
// ruleLightJudge 只依据廉价规则信号判断是否值得回复
// (设计/12 §6 策略二/五)。优先级:直接点名、提及主人的显示
// 名、高价值关键词,其次是安静时段偏向静默;否则
// 结论为无结论(交给注入的 LightJudge,没有则静默)。它
// 只读取已解析的 actor 元数据与消息内容——绝不推断
// 身份。
func (b *Bridge) ruleLightJudge(msg adapter.UnifiedMessage, actor adapter.Actor, inQuiet bool) lightVerdict {
// (设计/12 §6 策略二)。优先级:直接点名、提及主人的显示
// 名、高价值关键词;除此之外一律无结论(交给参与度引擎 /
// 注入的 LightJudge,没有则失败关闭为静默)。**安静时段不是
// 这里的静默理由**(主人 2026-10-05 裁决②:安静时段由轻思考判断
// 是否回复,不是硬性全静默)——该信号由调用方随 inQuiet 传给
// 下游判定器。它只读取已解析的消息信号(提及/正文)——绝不
// 推断身份(身份由 actor 元数据在会话层解析,红线)。
func (b *Bridge) ruleLightJudge(msg adapter.UnifiedMessage) lightVerdict {
if len(msg.Mentions) > 0 {
return verdictRespond
}
@@ -146,20 +149,26 @@ func (b *Bridge) ruleLightJudge(msg adapter.UnifiedMessage, actor adapter.Actor,
if hasHighValueKeyword(msg.Content) {
return verdictRespond
}
if inQuiet {
return verdictSilent
}
return verdictInconclusive
}
// isStrangerPrivate 报告该会话是否为**陌生人私聊**(非群 + 非主人)。
// 它只读会话层结构化解析出的标志(项目红线:身份不靠正文/提示词)。
// 主人私聊在 onMessage/shouldRespond 之前就已被 actor 门拦下,因此
// 这里成真的只会是陌生人私聊。
func isStrangerPrivate(sess session.Session) bool {
return !sess.IsGroup && !sess.Owner
}
// shouldRespond 实现非主人入站消息的最小打扰分级
// (设计/12 §6 策略一/二/五 + design/18 §2/§4/§5)。它返回该消息
// (设计/12 §6 策略一/二 + design/18 §2/§4/§5)。它返回该消息
// 应驱动完整 agent 循环并回复,还是只登记并静默。
// 主人消息绝不会到这里(processFrom 以 !actor.IsOwner 为门),
// 因此主人私聊与主人在群里的 @/高价值回复不受影响。sess 是
// 该消息解析出的隔离群聊/陌生人会话绑定,使参与度
// 引擎探测时能读到群的近期上下文。
func (b *Bridge) shouldRespond(ctx context.Context, msg adapter.UnifiedMessage, actor adapter.Actor, sess session.Session) (bool, string) {
// 引擎探测时能读到群的近期上下文;a 是收到该消息的账号适配器,
// 用于取该账号的流控闸门(陌生人私聊的强制限流)。
func (b *Bridge) shouldRespond(ctx context.Context, msg adapter.UnifiedMessage, actor adapter.Actor, sess session.Session, a adapter.PlatformAdapter) (bool, string) {
// 策略一:平台/系统噪声 → 只登记,绝不进完整循环。
if isLowValueEvent(actor) {
b.log.Debug("non-owner low-value system event, registered only",
@@ -167,18 +176,29 @@ func (b *Bridge) shouldRespond(ctx context.Context, msg adapter.UnifiedMessage,
return false, "low-value system event; registered only"
}
// 策略二/五:基于规则的轻量判定,感知安静时段。直接 @、
// 提及主人显示名或高价值关键词一律回复;安静时段
// 偏向静默。只有无结论的剩余部分才咨询
// 参与度引擎。
// 裁决①(主人 2026-10-05):**陌生人私聊 = 正常聊天** —— 不设安静期、
// 不走「值不值得回」的概率门,直接进入完整回合。唯一的闸门是强制
// 流控:同一发送者每分钟 ≤ FlowDirectDefault 条(AllowDirect 记账)
// 与同时最多 1 个在跑的回合(processFrom 用 AcquireTurn 施加)。
// 被限流时**不回**:调用方会把它登记进隔离会话,这里留一条带
// source 的可见告警(绝不静默)。
if isStrangerPrivate(sess) {
if g := b.gateFor(a); g != nil && !g.AllowDirect(msg) {
b.log.Warn("stranger private chat rate limited, registered only",
"platform", msg.Platform, "channel", msg.ChannelID, "sender", msg.SenderID,
"limit_per_min", adapter.FlowDirectDefault)
return false, "stranger private: over the per-sender flow limit"
}
return true, "stranger private chat: normal reply"
}
// 策略二/五:基于规则的轻量判定。直接 @、提及主人显示名或
// 高价值关键词一律直接回;规则得不出结论的部分交给参与度
// 引擎 / 轻量判定器。**安静时段只作为信号**(inQuiet)继续往下传,
// 规则层不再因为它静默(裁决②)。
inQuiet := b.inQuietHours(b.now())
switch b.ruleLightJudge(msg, actor, inQuiet) {
case verdictRespond:
if b.ruleLightJudge(msg) == verdictRespond {
return true, "rule signal: worth responding"
case verdictSilent:
b.log.Debug("non-owner message silenced (quiet/no signal), registered only",
"platform", msg.Platform, "channel", msg.ChannelID, "quiet", inQuiet, "actor_role", actor.Role)
return false, "rule signal: not worth responding"
}
// 廉价规则得不出结论。参与度引擎(design/18)已配置时
@@ -245,6 +265,18 @@ func (b *Bridge) registerSilenced(ctx context.Context, msg adapter.UnifiedMessag
"platform", msg.Platform, "channel", msg.ChannelID, "session", sess.SessionID)
}
// registerThrottled 是一次被强制流控(同一发送者已有回合在跑)拦下的
// 非主人消息的落点:先走最小打扰的登记路径(registerSilenced)把它写进
// 其隔离会话,再留一条**带 source 的可见告警** —— 被限流绝不静默
// (主人 2026-10-05 裁决③)。限流路径与普通静默因此区分得开:前者
// 一定有可见日志,后者只有 debug。
func (b *Bridge) registerThrottled(ctx context.Context, msg adapter.UnifiedMessage, actor adapter.Actor, sess session.Session, a adapter.PlatformAdapter, reason string) {
b.registerSilenced(ctx, msg, actor, sess, a)
b.log.Warn("inbound message throttled by flow gate, registered only",
"platform", msg.Platform, "channel", msg.ChannelID,
"sender", msg.SenderID, "reason", reason)
}
// inQuietHours 报告 at 是否落在配置的免打扰窗口内
// (与 backthink 语义一致:两个非零且相等的时刻关闭该窗口,
// 跨午夜的窗口也能正确处理)。它是 Bridge 上的纯方法,测试