docs(comments): 注释中文化 · internal/services/channelbridge(纯替换)
- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态) - 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避) - 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
This commit is contained in:
1 parent
61cf80d5ee
commit
093f38d986
28 files changed
+1089
-1089
No files matched your search
@@ -12,48 +12,48 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session"
|
||||
)
|
||||
|
||||
// This file holds the participation engine for public-platform group/stranger
|
||||
// conversations (DevDocs/design/18): a per-conversation state machine
|
||||
// (IDLE→PROBING→ACTIVE→COOLDOWN), a cost-controlled trigger gate (role-name mention
|
||||
// / probability), an activity window with follow-up review, and cold-avoid when the
|
||||
// group voices a negative attitude. It is in-memory per process (a restart resets it)
|
||||
// and consults the light LLM judge only on an actual trigger, so no chat message is
|
||||
// sent to an LLM just to decide whether to bother someone (a design/18 red line).
|
||||
// 本文件承载公开平台群聊/陌生人会话的参与度引擎
|
||||
// (DevDocs/design/18):按会话的状态机
|
||||
// (IDLE→PROBING→ACTIVE→COOLDOWN)、受成本控制的触发闸门(角色名提及
|
||||
// / 概率)、带跟进复审的活跃窗口,以及群内出现负面
|
||||
// 态度时的冷避。它按进程存于内存(重启即重置),
|
||||
// 且只在真正触发时才咨询轻量 LLM 判定器,因此不会仅为「要不要
|
||||
// 打扰某人」就把聊天消息发给 LLM(design/18 红线)。
|
||||
|
||||
// EngageConfig carries the design/18 per-conversation participation tuning. P2c will
|
||||
// make it per-group / panel-editable; for now DefaultEngageConfig supplies the
|
||||
// constants and the EngageFor seam is left nil in the composition root.
|
||||
// EngageConfig 承载 design/18 的按会话参与度调参。P2c 会让它
|
||||
// 可按群配置 / 在面板编辑;目前由 DefaultEngageConfig 提供
|
||||
// 常量,组合根中的 EngageFor 接缝保持 nil。
|
||||
type EngageConfig struct {
|
||||
// Enabled is the per-conversation switch (design/18 §6 engageEnabled). When false
|
||||
// only the role-name-reminder trigger still probes; the probability path is off.
|
||||
// Enabled 是按会话开关(design/18 §6 engageEnabled)。为 false 时
|
||||
// 只有角色名提醒词触发仍会探测;概率路径关闭。
|
||||
Enabled bool
|
||||
Probability float64 // 触发③ probability, default 0.10
|
||||
ActivityWindow time.Duration // ACTIVE window, default 15m
|
||||
ActivityMessages int // ACTIVE window by message count, default 20
|
||||
Cooldown time.Duration // COOLDOWN duration, default 30m
|
||||
ContextMessages int // how many recent messages the probe feeds the judge
|
||||
Probability float64 // 触发③ 概率,默认 0.10
|
||||
ActivityWindow time.Duration // ACTIVE 窗口,默认 15m
|
||||
ActivityMessages int // ACTIVE 窗口按消息条数计,默认 20
|
||||
Cooldown time.Duration // COOLDOWN 时长,默认 30m
|
||||
ContextMessages int // 探测时喂给判定器的近期消息条数
|
||||
}
|
||||
|
||||
// Knob defaults (design/18 §6). Compile-time so no bare magic number appears in the
|
||||
// run path (project rule §10).
|
||||
// 参数默认值(design/18 §6)。编译期常量,运行路径上就不会出现
|
||||
// 裸魔法数字(项目规范 §10)。
|
||||
const (
|
||||
defaultEngageProbability = 0.10
|
||||
defaultActivityWindow = 15 * time.Minute
|
||||
defaultActivityMessages = 20
|
||||
defaultCooldownDuration = 30 * time.Minute
|
||||
defaultEngageContext = 12
|
||||
// consecutiveNoInterestThreshold is how many follow-up probes in a row return "no
|
||||
// interest"/avoid before she exits ACTIVE back to IDLE (design/18 §4). A negative
|
||||
// attitude instead parks her in COOLDOWN via the rule list below.
|
||||
// consecutiveNoInterestThreshold 是连续多少次跟进探测都返回「无
|
||||
// 兴趣」/回避后,她从 ACTIVE 退回 IDLE(design/18 §4)。负面
|
||||
// 态度则由下面的规则词表把她按进 COOLDOWN。
|
||||
consecutiveNoInterestThreshold = 2
|
||||
)
|
||||
|
||||
// errNoEngageJudge is returned when the participation engine is configured but no
|
||||
// LightJudge was wired, so it fails closed to silence.
|
||||
// errNoEngageJudge 表示参与度引擎已配置但未装配 LightJudge,
|
||||
// 因此它失败关闭为静默。
|
||||
var errNoEngageJudge = errors.New("channelbridge: no light judge wired for engage")
|
||||
|
||||
// DefaultEngageConfig returns the design/18 knob defaults (P2a base; P2c renders them
|
||||
// per-group/panel editable).
|
||||
// DefaultEngageConfig 返回 design/18 的参数默认值(P2a 基准;P2c 会让它们
|
||||
// 可按群配置/在面板编辑)。
|
||||
func DefaultEngageConfig() EngageConfig {
|
||||
return EngageConfig{
|
||||
Enabled: true,
|
||||
@@ -65,18 +65,18 @@ func DefaultEngageConfig() EngageConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// engageState is one phase of the per-conversation state machine (design/18 §1).
|
||||
// engageState 是按会话状态机的一个阶段(design/18 §1)。
|
||||
type engageState int
|
||||
|
||||
const (
|
||||
engageIDLE engageState = iota
|
||||
engagePROBING // transient: a trigger just routed this message to the judge
|
||||
engageACTIVE // she is participating; follow-up review is on
|
||||
engageCOOLDOWN // she was asked to be quiet; auto-resumes when the timer ends
|
||||
engagePROBING // 瞬态:某次触发刚把该消息送去判定
|
||||
engageACTIVE // 她正在参与;跟进复审已开启
|
||||
engageCOOLDOWN // 她被要求安静;计时结束后自动恢复
|
||||
)
|
||||
|
||||
// engageGroupState is one participation-engine instance, keyed by a group/stranger
|
||||
// conversation (design/18 §1). It is in-memory only (a process restart resets it).
|
||||
// engageGroupState 是一个参与度引擎实例,以群聊/陌生人
|
||||
// 会话为键(design/18 §1)。它仅存于内存(进程重启即重置)。
|
||||
type engageGroupState struct {
|
||||
mu sync.Mutex
|
||||
state engageState
|
||||
@@ -88,10 +88,10 @@ type engageGroupState struct {
|
||||
consecutiveNoInterest int
|
||||
}
|
||||
|
||||
// engageConfigured reports whether the design/18 participation engine is wired for
|
||||
// this bridge at all. It is true when the persona carries trigger words or the engage
|
||||
// knobs enable the engine; otherwise the bridge keeps the pre-engage legacy light
|
||||
// judgement (design/12 §6) untouched.
|
||||
// engageConfigured 报告该桥接器是否装配了 design/18 参与度引擎。
|
||||
// 角色带触发词,或参与度参数启用引擎时为 true;
|
||||
// 否则桥接器沿用引入参与度引擎之前的旧轻量判定
|
||||
// (design/12 §6),不做改动。
|
||||
func (b *Bridge) engageConfigured() bool {
|
||||
return len(b.triggerWords) > 0 || b.engage.Enabled || b.engage.Probability > 0
|
||||
}
|
||||
@@ -107,8 +107,8 @@ func engageKey(msg adapter.UnifiedMessage, actor adapter.Actor) string {
|
||||
return adapter.ConversationKey(msg)
|
||||
}
|
||||
|
||||
// engageState returns the state for a conversation key, creating an IDLE instance on
|
||||
// first use (in-memory, safe for concurrent use).
|
||||
// engageState 返回会话键对应的状态,首次使用时创建 IDLE
|
||||
// 实例(内存态,可安全并发使用)。
|
||||
func (b *Bridge) engageState(key string) *engageGroupState {
|
||||
b.engageMu.Lock()
|
||||
defer b.engageMu.Unlock()
|
||||
@@ -120,9 +120,9 @@ func (b *Bridge) engageState(key string) *engageGroupState {
|
||||
return st
|
||||
}
|
||||
|
||||
// engageConfigFor resolves the effective engage config for a message: the per-group
|
||||
// override (EngageFor, nil in P2a) when one is wired, else the base config, with any
|
||||
// zero field normalized to its default.
|
||||
// engageConfigFor 解析一条消息实际生效的参与度配置:装配了按群
|
||||
// 覆盖(EngageFor,P2a 中为 nil)时用它,否则用基准配置,
|
||||
// 并把所有零值字段归一化为默认值。
|
||||
func (b *Bridge) engageConfigFor(msg adapter.UnifiedMessage, actor adapter.Actor) EngageConfig {
|
||||
cfg := b.engage
|
||||
if b.engageFor != nil {
|
||||
@@ -134,8 +134,8 @@ func (b *Bridge) engageConfigFor(msg adapter.UnifiedMessage, actor adapter.Actor
|
||||
cfg = *ov
|
||||
}
|
||||
}
|
||||
// Normalize unset fields to the sheltered defaults so a trigger-only config still
|
||||
// bounds the activity window and cooldown instead of running with zero values.
|
||||
// 把未设置的字段归一化为保守默认值,使只配了触发的配置仍能
|
||||
// 限定活跃窗口与冷却,而不是以零值运行。
|
||||
if cfg.ActivityWindow <= 0 {
|
||||
cfg.ActivityWindow = defaultActivityWindow
|
||||
}
|
||||
@@ -154,12 +154,12 @@ func (b *Bridge) engageConfigFor(msg adapter.UnifiedMessage, actor adapter.Actor
|
||||
return cfg
|
||||
}
|
||||
|
||||
// engageDecision is the design/18 participation decision for an inconclusive (to the
|
||||
// cheap rules) non-owner message. It is only reached when engageConfigured() is true.
|
||||
// It returns whether the message should drive the full agent loop and reply, plus a
|
||||
// short reason for the log. The caller (shouldRespond) holds no lock; the per-key
|
||||
// state mutex is acquired here so concurrent messages for one conversation serialize,
|
||||
// while different conversations stay independent.
|
||||
// engageDecision 是对(廉价规则无法判定)非主人消息的 design/18
|
||||
// 参与度决策。仅在 engageConfigured() 为 true 时才会走到。
|
||||
// 它返回该消息是否应驱动完整 agent 循环并回复,以及
|
||||
// 供日志用的简短原因。调用方(shouldRespond)不持锁;
|
||||
// 这里获取按键的状态互斥锁,使同一会话的并发消息串行化,
|
||||
// 不同会话之间保持独立。
|
||||
func (b *Bridge) engageDecision(ctx context.Context, msg adapter.UnifiedMessage, actor adapter.Actor, sess session.Session, inQuiet bool) (bool, string) {
|
||||
cfg := b.engageConfigFor(msg, actor)
|
||||
key := engageKey(msg, actor)
|
||||
@@ -168,10 +168,10 @@ func (b *Bridge) engageDecision(ctx context.Context, msg adapter.UnifiedMessage,
|
||||
defer st.mu.Unlock()
|
||||
now := b.now()
|
||||
|
||||
// design/18 §5 cold-avoid first: a group member voicing a negative attitude is a
|
||||
// hard "be quiet" signal → COOLDOWN, even mid-engagement. A direct @ / high-value
|
||||
// keyword already returned above, so only genuinely "worth a reply by content"
|
||||
// (or an owner message, which never reaches here) can override it.
|
||||
// design/18 §5 冷避优先:群成员表达负面态度是
|
||||
// 明确的「安静」信号 → COOLDOWN,即使正处在参与中。直接 @ / 高价值
|
||||
// 关键词已在上方返回,因此只有真正「内容值得回复」
|
||||
// 的消息(或主人消息,它根本不会走到这里)才能覆盖它。
|
||||
if b.negativeAttitude(msg.Content) {
|
||||
b.enterCooldown(st, cfg, now)
|
||||
b.log.Debug("engage cooldown (negative attitude)",
|
||||
@@ -185,8 +185,8 @@ func (b *Bridge) engageDecision(ctx context.Context, msg adapter.UnifiedMessage,
|
||||
return false, "engage: cooldown active"
|
||||
}
|
||||
|
||||
// ACTIVE follow-up: while the activity window is live she keeps lightly judging
|
||||
// the group's continuing discussion and tends to stay engaged (design/18 §4).
|
||||
// ACTIVE 跟进:活跃窗口未过期间,她持续轻量判断
|
||||
// 群里延续的讨论,并倾向于保持参与(design/18 §4)。
|
||||
if st.state == engageACTIVE {
|
||||
if st.activityExpired(now, cfg) {
|
||||
b.toIdle(st)
|
||||
@@ -196,9 +196,9 @@ func (b *Bridge) engageDecision(ctx context.Context, msg adapter.UnifiedMessage,
|
||||
}
|
||||
}
|
||||
|
||||
// IDLE: only a role-name mention or a probability hit routes this message into a
|
||||
// cost-controlled PROBING judgment (design/18 §2 触发②/③); otherwise it is
|
||||
// registered and silenced with no LLM call (a red line).
|
||||
// IDLE:只有角色名提及或概率命中才会把该消息送入受成本控制的
|
||||
// PROBING 判定(design/18 §2 触发②/③);否则它只被
|
||||
// 登记并静默,不调用 LLM(一条红线)。
|
||||
if !b.mentionsTriggerWord(msg.Content) && !b.engageProbabilityHit(cfg) {
|
||||
b.log.Debug("engage no trigger, registered only", "group", key)
|
||||
return false, "engage: no trigger"
|
||||
@@ -206,9 +206,9 @@ func (b *Bridge) engageDecision(ctx context.Context, msg adapter.UnifiedMessage,
|
||||
return b.engageProbe(ctx, msg, actor, sess, key, st, cfg, inQuiet)
|
||||
}
|
||||
|
||||
// engageProbe is one cost-controlled PROBING judgment (design/18 §3): it feeds the
|
||||
// group context + persona prompt to the light judge and decides ACTIVE (reply) or
|
||||
// IDLE (registered silent). Parsing/LLM failure fails closed to silence.
|
||||
// engageProbe 是一次受成本控制的 PROBING 判定(design/18 §3):把
|
||||
// 群上下文 + 角色提示词喂给轻量判定器,并决定 ACTIVE(回复)还是
|
||||
// IDLE(仅登记、静默)。解析/LLM 失败时失败关闭为静默。
|
||||
func (b *Bridge) engageProbe(ctx context.Context, msg adapter.UnifiedMessage, actor adapter.Actor, sess session.Session, key string, st *engageGroupState, cfg EngageConfig, inQuiet bool) (bool, string) {
|
||||
now := b.now()
|
||||
st.state = engagePROBING
|
||||
@@ -233,10 +233,10 @@ func (b *Bridge) engageProbe(ctx context.Context, msg adapter.UnifiedMessage, ac
|
||||
return false, "engage: no interest"
|
||||
}
|
||||
|
||||
// engageFollowUp is the ACTIVE-window follow-up review (design/18 §4): she still
|
||||
// lightly judges the discussion but leans toward continuing. A run of no-interest or a
|
||||
// finished window drops her out of ACTIVE; a negative attitude was already caught
|
||||
// earlier by the cold-avoid rule.
|
||||
// engageFollowUp 是 ACTIVE 窗口的跟进复审(design/18 §4):她仍
|
||||
// 轻量判断讨论,但倾向于继续参与。连续无兴趣或
|
||||
// 窗口结束会使她退出 ACTIVE;负面态度早已
|
||||
// 被前面的冷避规则捕获。
|
||||
func (b *Bridge) engageFollowUp(ctx context.Context, msg adapter.UnifiedMessage, actor adapter.Actor, sess session.Session, key string, st *engageGroupState, cfg EngageConfig, inQuiet bool) (bool, string) {
|
||||
st.msgsInWindow++
|
||||
now := b.now()
|
||||
@@ -258,7 +258,7 @@ func (b *Bridge) engageFollowUp(ctx context.Context, msg adapter.UnifiedMessage,
|
||||
return true, "engage follow-up: " + d.Reason
|
||||
}
|
||||
|
||||
// engageAfterFollowUp resolves the outcome of a no-interest/avoid follow-up probe.
|
||||
// engageAfterFollowUp 判定一次无兴趣/回避跟进探测的结果。
|
||||
func (b *Bridge) engageAfterFollowUp(st *engageGroupState, cfg EngageConfig, now time.Time, key string) (bool, string) {
|
||||
if st.consecutiveNoInterest >= consecutiveNoInterestThreshold || st.activityExpired(now, cfg) {
|
||||
b.toIdle(st)
|
||||
@@ -268,14 +268,14 @@ func (b *Bridge) engageAfterFollowUp(st *engageGroupState, cfg EngageConfig, now
|
||||
return false, "engage: not responding"
|
||||
}
|
||||
|
||||
// countNoInterest bumps the consecutive no-interest counter for a follow-up probe.
|
||||
// countNoInterest 为一次跟进探测递增连续无兴趣计数。
|
||||
func (b *Bridge) countNoInterest(st *engageGroupState) {
|
||||
st.consecutiveNoInterest++
|
||||
}
|
||||
|
||||
// judgeEngage renders the LightInput for a cost-controlled participation judgment and
|
||||
// consults the injected LightJudge. It feeds the group's recent context and the
|
||||
// persona prompt only here — never on an untriggered message.
|
||||
// judgeEngage 为一次受成本控制的参与度判定渲染 LightInput,并
|
||||
// 咨询注入的 LightJudge。只有在这里才喂入群的近期上下文与
|
||||
// 角色提示词——绝不在未触发的消息上喂。
|
||||
func (b *Bridge) judgeEngage(ctx context.Context, msg adapter.UnifiedMessage, actor adapter.Actor, sess session.Session, cfg EngageConfig, inQuiet bool) (LightDecision, error) {
|
||||
if b.light == nil {
|
||||
return LightDecision{}, errNoEngageJudge
|
||||
@@ -290,10 +290,10 @@ func (b *Bridge) judgeEngage(ctx context.Context, msg adapter.UnifiedMessage, ac
|
||||
})
|
||||
}
|
||||
|
||||
// buildEngageContext renders the conversation's most recent N messages with sender
|
||||
// names, oldest-first, for the engagement judge. It is a no-op (empty string) when no
|
||||
// history store is wired or the store has no rows yet, so the judge degrades to a
|
||||
// single-trigger-message read.
|
||||
// buildEngageContext 为参与度判定器渲染该会话最近的 N 条消息(带发送者
|
||||
// 名,最旧在前)。未装配历史存储或存储尚无数据时为空操作
|
||||
// (返回空字符串),判定器因此退化为只读
|
||||
// 那一条触发消息。
|
||||
func (b *Bridge) buildEngageContext(ctx context.Context, sess session.Session, cfg EngageConfig) string {
|
||||
if b.history == nil || cfg.ContextMessages <= 0 {
|
||||
return ""
|
||||
@@ -320,10 +320,10 @@ func (b *Bridge) buildEngageContext(ctx context.Context, sess session.Session, c
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// engageProbabilityHit reports whether this message independently draws the
|
||||
// role-name-reminder-free probability trigger (design/18 §2 触发③). It reads the
|
||||
// injectable EngagRand source so tests are deterministic; nil randomness keeps the
|
||||
// probability path off (fail-closed).
|
||||
// engageProbabilityHit 报告该消息是否独立命中与角色名提醒词无关的
|
||||
// 概率触发(design/18 §2 触发③)。它读取可注入的
|
||||
// EngagRand 源,使测试具备确定性;随机源为 nil 时概率路径
|
||||
// 关闭(失败关闭)。
|
||||
func (b *Bridge) engageProbabilityHit(cfg EngageConfig) bool {
|
||||
if !cfg.Enabled || cfg.Probability <= 0 || b.engageRand == nil {
|
||||
return false
|
||||
@@ -331,9 +331,9 @@ func (b *Bridge) engageProbabilityHit(cfg EngageConfig) bool {
|
||||
return b.engageRand() < cfg.Probability
|
||||
}
|
||||
|
||||
// negativeAttitude reports whether the group is telling her to be quiet (design/18 §5
|
||||
// cold-avoid). It is a cheap substring rule that backs off anything the LLM might miss
|
||||
// — being quieter is the safe direction (不打扰为第一原则). Never used for identity.
|
||||
// negativeAttitude 报告群是否在让她安静(design/18 §5
|
||||
// 冷避)。这是一条廉价子串规则,兜住 LLM 可能漏掉的信号
|
||||
// ——更安静是安全的方向(不打扰为第一原则)。绝不用于身份判定。
|
||||
func (b *Bridge) negativeAttitude(text string) bool {
|
||||
if text == "" {
|
||||
return false
|
||||
@@ -347,15 +347,15 @@ func (b *Bridge) negativeAttitude(text string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// negativeAttitudeKeywords is the cold-avoid rule list (design/18 §5 词表兜底). It is a
|
||||
// fallback; the same signal is read by the follow-up review in the judge via avoidTopic.
|
||||
// negativeAttitudeKeywords 是冷避规则词表(design/18 §5 词表兜底)。它只是
|
||||
// 兜底;同一信号也会被判定器中的跟进复审通过 avoidTopic 读取。
|
||||
var negativeAttitudeKeywords = []string{
|
||||
"太吵", "吵死", "闭嘴", "别吵", "别刷屏", "别刷", "刷屏", "不想听",
|
||||
"安静点", "少说两句", "别说了", "别bb", "别逼逼", "烦死了", "好烦",
|
||||
"打住", "住嘴", "shut up", "too loud", "stop spamming",
|
||||
}
|
||||
|
||||
// contentSnippet shortens a message to a bounded topic token for the state machine.
|
||||
// contentSnippet 把消息截短为状态机所用的有界话题标记。
|
||||
func contentSnippet(text string) string {
|
||||
runes := []rune(strings.TrimSpace(text))
|
||||
if len(runes) <= 40 {
|
||||
@@ -372,7 +372,7 @@ func (st *engageGroupState) activityExpired(now time.Time, cfg EngageConfig) boo
|
||||
return now.After(st.activeUntil) || st.msgsInWindow >= cfg.ActivityMessages
|
||||
}
|
||||
|
||||
// toIdle resets the conversation back to IDLE (design/18: exit ACTIVE → IDLE).
|
||||
// toIdle 把会话重置回 IDLE(design/18:退出 ACTIVE → IDLE)。
|
||||
func (b *Bridge) toIdle(st *engageGroupState) {
|
||||
st.state = engageIDLE
|
||||
st.msgsInWindow = 0
|
||||
@@ -380,7 +380,7 @@ func (b *Bridge) toIdle(st *engageGroupState) {
|
||||
st.lastTopic = ""
|
||||
}
|
||||
|
||||
// enterActive opens the activity window (design/18 §4). The caller holds st.mu.
|
||||
// enterActive 打开活跃窗口(design/18 §4)。调用方须持有 st.mu。
|
||||
func (b *Bridge) enterActive(st *engageGroupState, cfg EngageConfig, now time.Time, msg adapter.UnifiedMessage) {
|
||||
st.state = engageACTIVE
|
||||
st.activeUntil = now.Add(cfg.ActivityWindow)
|
||||
@@ -390,8 +390,8 @@ func (b *Bridge) enterActive(st *engageGroupState, cfg EngageConfig, now time.Ti
|
||||
st.lastTopic = contentSnippet(msg.Content)
|
||||
}
|
||||
|
||||
// enterCooldown parks the conversation in a quiet period (design/18 §5). The caller
|
||||
// holds st.mu.
|
||||
// enterCooldown 把会话按进安静期(design/18 §5)。调用方
|
||||
// 须持有 st.mu。
|
||||
func (b *Bridge) enterCooldown(st *engageGroupState, cfg EngageConfig, now time.Time) {
|
||||
st.state = engageCOOLDOWN
|
||||
st.cooldownUntil = now.Add(cfg.Cooldown)
|
||||
|
||||
Reference in new issue
Block a user