Files
Cyrene_v2/internal/services/channelbridge/transcript.go
T
AskaEth 7ae4298871
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(channels): 陌生人私聊正常聊天(跳概率门)+ 强制流控 6条/分与回合槽位 + 安静时段不再硬静默 + 受限车道边界行前置
- 陌生人私聊:不再走「值不值得回」的概率门,唯一闸门 = 每发送者 6 条/分滑窗 + 同会话 1 个在跑回合
- 被限流只登记不回,且留带 source 的可见告警(不静默)
- 安静时段在回复侧只作信号(规则层不再静默),群聊取消夜间静默
- 锁定流控常量:FlowDirectDefault 加字面量钉桩测试(放大它 = 静默关流控,且会让额度循环跑百万轮)
2026-10-05 13:26:15 +08:00

105 lines
6.3 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 (
"context"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/transcript"
)
// 本文件让 IM 回合带上**该会话自己**最近的对话历史,使她看得懂一段连贯的对话,而不是只
// 听得到当次那一条消息(主人 2026-10-05 反馈:「只有 @ 才有反应、不 @ 就不理解意图」——
// 此前群里的回合只有「来源系统行 + 当次消息」,前几句她根本没看见)。
//
// 会话环境在 loop.SessionEnv 上早已按车道解析(ActorEnv/MemWritable/PersonaID,
// design/22 §7-3/§7-4),缺的一直是**历史转录**:Web 主会话回合由编排器播种该会话最近 N 条
// 转录(gateway/session_transcript.go),而 IM 回合此前没有。
//
// - **主人私聊**:IM 回合镜像进主会话,因此播种**主会话**(`web`)的转录,与 Web 主会话
// 看到的历史逐条一致 —— 她不会像第一次见到主人(主人 2026-10-05 要求两条车道环境一致)。
// - **群聊 / 陌生人(受限车道)**:播种**该隔离会话自己**(`sess-*`)的最近转录,因此群里
// 前几句、以及被最小打扰静默掉而只登记不回的那几条(light.go 的 registerSilenced 已把它们
// 落进同一会话)都在她的上下文里,她据此才能理解一段连贯对话的意图。**顺序为
// frame 先行**:来源行 + 第三方身份边界在最前、转录居中、当次消息在最后
// (主人 2026-10-05 裁定,见 seedTurnTranscript)。
//
// 两条车道读的会话不同,重建逻辑与条数上限却是同一份:internal/runtime/transcript
// (ContextCount / Read / Message)—— 与网关同一份实现、同一个常量,绝不另写第二套
// (两份映射必然漂移)。**受限车道结构上够不到主会话**:目标会话由 inboundTargetFor 结构化
// 决定,受限车道拿到的是 `sess-*` 隔离会话,主人的主会话历史一个字都不外泄
// (design/16 §4.6 §3、design/02 §3)。
// turnEnv 返回一条入站消息本轮运行所用的 loop 会话环境。
//
// - 主人私聊(inboundTarget.private)与 Web 主会话是**同一条车道**,因此经主人车道的
// 唯一构造入口 loop.OwnerEnvFor 派生:两条车道逐字段同源,SessionEnv 将来新增字段时
// 会一起拿到,而不是各自漂移出「同一个她、两套环境」。
// - 受限会话(群聊/陌生人)由结构化解析出的隔离边界决定:记忆只读(MemWritable=false)、
// 并强制默认角色(design/22 §7-4 —— resolvePersona 对受限会话恒返回 "")。身份与权限
// 始终来自 actor 元数据,绝不来自这里的任何字符串(项目红线)。
func turnEnv(sess session.Session, target inboundTarget, personaID string) loop.SessionEnv {
if target.private {
return loop.OwnerEnvFor(personaID)
}
return loop.SessionEnv{ActorEnv: sess.Env(), MemWritable: sess.MemWritable, PersonaID: personaID}
}
// seedTurnTranscript 在本回合模型可见消息之前前置**该会话自己**最近的转录
// (与网关编排器的 startTurnMessages 同序:转录在前、本次消息在后):
//
// - 主人私聊 → 主会话(`web`)的转录,与 Web 主会话逐条一致;
// - 群聊 / 陌生人 → 该隔离会话(`sess-*`)的转录,含被静默登记的群成员发言。
//
// **受限车道的顺序是「frame 先行」**(主人 2026-10-05 裁定):来源系统行 +
// 第三方身份边界必须排在**最前**,转录居中,当次用户消息在最后——即
// `[边界/来源系统行, …转录…, 当次用户消息]`。理由是提示词注入面:若把转录
// 前置在边界之前,群友的历史发言会先于「这是群、这些人是第三方」进入她的
// 上下文。主人私聊车道**顺序逐字不变**(转录最前、来源行、当次消息),因为
// 那条车道的上下文隔离本就不同。
//
// 时刻与网关一致:调用点在本回合用户消息落库**之前**(runInboundTurn 里装配先于
// persistToMain / persistIsolated),因此当次消息绝不会在上下文里出现两次。
//
// 转录读不出来(未接历史存储、读失败、空会话)时原样返回,这正是既有行为:回合退化为
// 「来源行 + 当次消息」,而不是让回合失败。
//
// 受限车道另设一道显式守卫:目标会话是主会话 id 时宁可不播种。这在生产里结构上不可能
// (inboundTargetFor 只在主人私聊时给出 MainSessionID),但上下文隔离是安全红线 —— 万一
// 将来多出别的构造点写错,也绝不能把主人的主会话历史灌进群聊/陌生人上下文
// (design/02 §3、design/16 §4.6 §3)。
func (b *Bridge) seedTurnTranscript(ctx context.Context, target inboundTarget, msgs []model.Message) []model.Message {
if !target.private && target.sessionID == history.MainSessionID {
return msgs
}
seed := transcript.Read(ctx, b.history, target.sessionID)
if len(seed) == 0 {
return msgs
}
// 主人私聊:转录前置在最前(既有顺序,逐字不变)。
if target.private {
out := make([]model.Message, 0, len(seed)+len(msgs))
out = append(out, seed...)
return append(out, msgs...)
}
return insertSeedAfterFrame(msgs, seed)
}
// insertSeedAfterFrame 把受限车道的转录插在**最前面的系统框架行之后**
// (来源行 + 第三方身份边界仍在最前,frame 先行),当次用户消息保持在最后:
// `[系统框架行, …转录…, 当次用户消息]`。受限车道的本回合消息恰好是
// 「框架系统行 + 当次用户消息」,因此这一插入位置等价于「边界最前、历史居中、
// 当次最后」,且**当次消息绝不重复**。
func insertSeedAfterFrame(msgs, seed []model.Message) []model.Message {
frame := 0
for frame < len(msgs) && msgs[frame].Role == model.RoleSystem {
frame++
}
out := make([]model.Message, 0, len(msgs)+len(seed))
out = append(out, msgs[:frame]...)
out = append(out, seed...)
return append(out, msgs[frame:]...)
}