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
@@ -11,9 +11,9 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/user"
|
||||
)
|
||||
|
||||
// engageScriptJudge is a configurable LightJudge for the participation-engine tests:
|
||||
// it replays an ordered list of decisions (and optional per-call errors) and records
|
||||
// how often it was consulted plus the last input it saw.
|
||||
// engageScriptJudge 是参与度引擎测试用的可配置 LightJudge:
|
||||
// 它按顺序回放一组裁决(以及可选的逐次调用错误),并记录
|
||||
// 被咨询的次数以及最近一次看到的输入。
|
||||
type engageScriptJudge struct {
|
||||
responses []LightDecision
|
||||
errs []error
|
||||
@@ -34,15 +34,15 @@ func (s *engageScriptJudge) Judge(_ context.Context, in LightInput) (LightDecisi
|
||||
return LightDecision{Interesting: true, WorthReply: true, Reason: "默认参与"}, nil
|
||||
}
|
||||
|
||||
// dayNow returns a daytime (non-quiet) wall-clock source so the engine runs outside
|
||||
// the quiet window.
|
||||
// dayNow 返回白天(非安静)的墙上时钟源,使引擎运行在
|
||||
// 安静窗口之外。
|
||||
func dayNow() func() time.Time {
|
||||
return func() time.Time { return time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) }
|
||||
}
|
||||
|
||||
// engageTestBridge builds a Bridge wired for the participation engine with the given
|
||||
// judge/config/clock/rand source. A zero-valued config is replaced by the defaults so
|
||||
// callers can override just the knob they care about.
|
||||
// engageTestBridge 构建一个已接好参与度引擎的 Bridge,使用给定的
|
||||
// 判定器/配置/时钟/随机源。零值配置会被替换为默认值,
|
||||
// 使调用方只需覆盖自己关心的参数。
|
||||
func engageTestBridge(t *testing.T, judge LightJudge, cfg EngageConfig, clock func() time.Time, rng func() float64) (*Bridge, *stubChatter, *captureHistory) {
|
||||
t.Helper()
|
||||
chat := &stubChatter{res: &loop.Result{Text: "参与"}}
|
||||
@@ -52,7 +52,7 @@ func engageTestBridge(t *testing.T, judge LightJudge, cfg EngageConfig, clock fu
|
||||
cfg = DefaultEngageConfig()
|
||||
}
|
||||
if rng == nil {
|
||||
rng = func() float64 { return 0.5 } // default: never hit the probability trigger
|
||||
rng = func() float64 { return 0.5 } // 默认:永不命中概率触发
|
||||
}
|
||||
b, err := New(Options{
|
||||
Loop: chat, Sessions: sess, Owner: &user.OwnerProfile{}, History: hist,
|
||||
@@ -65,8 +65,8 @@ func engageTestBridge(t *testing.T, judge LightJudge, cfg EngageConfig, clock fu
|
||||
return b, chat, hist
|
||||
}
|
||||
|
||||
// cfgShortWindow is a config with a 1h cooldown/activity and a small 2-message
|
||||
// activity window, so count-based window expiry is easy to drive deterministically.
|
||||
// cfgShortWindow 是冷却/活跃时长 1 小时、活跃窗口仅 2 条消息
|
||||
// 的配置,便于确定性地驱动按条数计的窗口过期。
|
||||
func cfgShortWindow() EngageConfig {
|
||||
cfg := DefaultEngageConfig()
|
||||
cfg.ActivityWindow = time.Hour
|
||||
@@ -81,7 +81,7 @@ func TestEngageRoleNameMentionProbesAndResponds(t *testing.T) {
|
||||
judge := &engageScriptJudge{}
|
||||
b, chat, _ := engageTestBridge(t, judge, DefaultEngageConfig(), dayNow(), nil)
|
||||
|
||||
// 触发②: naming 昔涟 without an @ routes the message into a cost-controlled PROBING.
|
||||
// 触发②:没有 @ 但提到「昔涟」会把该消息送入受成本控制的 PROBING。
|
||||
b.process(context.Background(), nonOwnerGroupMsg("大家在聊昔涟的事"))
|
||||
|
||||
if judge.calls != 1 {
|
||||
@@ -109,7 +109,7 @@ func TestEngageRoleNameMentionNotInterestedSilences(t *testing.T) {
|
||||
if chat.sessionID != "" {
|
||||
t.Fatalf("loop ran after no-interest, session = %q", chat.sessionID)
|
||||
}
|
||||
// Not interested → registered but not answered.
|
||||
// 不感兴趣 → 只登记、不回复。
|
||||
if len(hist.records()) != 1 || hist.records()[0].Content != "提一句昔涟而已" {
|
||||
t.Fatalf("records = %+v, want 1 registered user message", hist.records())
|
||||
}
|
||||
@@ -134,8 +134,8 @@ func TestEngageAvoidTopicStaysIdle(t *testing.T) {
|
||||
func TestEngageNoTriggerNoProbabilitySkipsJudge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Cost-control red line: an untriggered, probability-missing message must NOT call
|
||||
// the light judge at all — no context is fed, no LLM turn, just registered silent.
|
||||
// 成本控制红线:未触发、也未命中概率的消息绝不调用
|
||||
// 轻量判定器——不喂上下文、不走 LLM 轮次,只登记并静默。
|
||||
judge := &engageScriptJudge{}
|
||||
b, chat, hist := engageTestBridge(t, judge, DefaultEngageConfig(), dayNow(), nil)
|
||||
|
||||
@@ -155,8 +155,8 @@ func TestEngageNoTriggerNoProbabilitySkipsJudge(t *testing.T) {
|
||||
func TestEngageProbabilityTriggerResponds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// 触发③: an injected rand below the probability (0.05 < 0.10) routes the message
|
||||
// into PROBING even with no role-name mention.
|
||||
// 触发③:注入的随机值低于概率阈值(0.05 < 0.10),即使没有
|
||||
// 角色名提及也会把消息送入 PROBING。
|
||||
judge := &engageScriptJudge{}
|
||||
b, chat, _ := engageTestBridge(t, judge, DefaultEngageConfig(), dayNow(), func() float64 { return 0.05 })
|
||||
|
||||
@@ -190,25 +190,25 @@ func TestEngageStateMachineActivityWindowExpiry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
judge := &engageScriptJudge{responses: []LightDecision{
|
||||
{Interesting: true, WorthReply: true, Reason: "感兴趣"}, // probe → ACTIVE
|
||||
{WorthReply: true, Reason: "跟随"}, // follow-up 1 → still reply
|
||||
{Interesting: true, WorthReply: true, Reason: "感兴趣"}, // 探测 → ACTIVE
|
||||
{WorthReply: true, Reason: "跟随"}, // 跟进 1 → 仍然回复
|
||||
}}
|
||||
b, chat, _ := engageTestBridge(t, judge, cfgShortWindow(), dayNow(), nil)
|
||||
|
||||
// msg1: role-name mention → probe → interested → ACTIVE (respond).
|
||||
// msg1:角色名提及 → 探测 → 感兴趣 → ACTIVE(回复)。
|
||||
b.process(context.Background(), nonOwnerGroupMsg("聊昔涟"))
|
||||
if chat.sessionID != "sess-g" || judge.calls != 1 {
|
||||
t.Fatalf("after probe: session=%q calls=%d, want sess-g/1", chat.sessionID, judge.calls)
|
||||
}
|
||||
|
||||
// msg2: inside the window → follow-up review, still replies.
|
||||
// msg2:仍在窗口内 → 跟进复审,仍回复。
|
||||
b.process(context.Background(), nonOwnerGroupMsg("周末去哪玩"))
|
||||
if chat.sessionID != "sess-g" || judge.calls != 2 {
|
||||
t.Fatalf("after follow-up: session=%q calls=%d, want sess-g/2", chat.sessionID, judge.calls)
|
||||
}
|
||||
|
||||
// msg3: the 2-message window is now full → exit ACTIVE to IDLE. No trigger/probability
|
||||
// (rand 0.5) → silent, and crucially the judge is NOT consulted a third time.
|
||||
// msg3:2 条消息的窗口已满 → 退出 ACTIVE 回到 IDLE。无触发/无概率
|
||||
// (随机值 0.5)→ 静默,且关键是判定器不会第三次被咨询。
|
||||
b.process(context.Background(), nonOwnerGroupMsg("接着聊"))
|
||||
if judge.calls != 2 {
|
||||
t.Fatalf("judge calls = %d, want 2 (window expired; no follow-up, no re-trigger)", judge.calls)
|
||||
@@ -223,9 +223,9 @@ func TestEngageStateMachineFollowUpNoInterestExits(t *testing.T) {
|
||||
cfg.ActivityMessages = 100
|
||||
cfg.Cooldown = time.Hour
|
||||
judge := &engageScriptJudge{responses: []LightDecision{
|
||||
{Interesting: true, WorthReply: true, Reason: "感兴趣"}, // probe → ACTIVE
|
||||
{Interesting: false, WorthReply: false, Reason: "无感"}, // follow-up 1 → no interest
|
||||
{Interesting: false, WorthReply: false, Reason: "无感"}, // follow-up 2 → threshold → exit
|
||||
{Interesting: true, WorthReply: true, Reason: "感兴趣"}, // 探测 → ACTIVE
|
||||
{Interesting: false, WorthReply: false, Reason: "无感"}, // 跟进 1 → 无兴趣
|
||||
{Interesting: false, WorthReply: false, Reason: "无感"}, // 跟进 2 → 达到阈值 → 退出
|
||||
}}
|
||||
b, chat, _ := engageTestBridge(t, judge, cfg, dayNow(), nil)
|
||||
|
||||
@@ -233,8 +233,8 @@ func TestEngageStateMachineFollowUpNoInterestExits(t *testing.T) {
|
||||
if judge.calls != 1 || chat.sessionID != "sess-g" {
|
||||
t.Fatalf("probe: calls=%d session=%q, want 1/sess-g", judge.calls, chat.sessionID)
|
||||
}
|
||||
// Two consecutive no-interest follow-ups, then a subsequent untriggered message
|
||||
// must no longer call the judge (she has dropped out of ACTIVE).
|
||||
// 连续两次无兴趣跟进之后,随后一条未触发的消息
|
||||
// 绝不再调用判定器(她已退出 ACTIVE)。
|
||||
b.process(context.Background(), nonOwnerGroupMsg("说说别的"))
|
||||
b.process(context.Background(), nonOwnerGroupMsg("再说一点"))
|
||||
if judge.calls != 3 {
|
||||
@@ -251,10 +251,10 @@ func TestEngageColdAvoidCooldownSuppressesThenExpires(t *testing.T) {
|
||||
|
||||
now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
|
||||
clock := func() time.Time { return now }
|
||||
judge := &engageScriptJudge{} // default: interested (would reply if consulted)
|
||||
judge := &engageScriptJudge{} // 默认:感兴趣(若被咨询会回复)
|
||||
b, chat, _ := engageTestBridge(t, judge, DefaultEngageConfig(), clock, nil)
|
||||
|
||||
// 冷避: "别刷屏" is a negative attitude → COOLDOWN, and she is silent (registered only).
|
||||
// 冷避:「别刷屏」是负面态度 → COOLDOWN,她保持静默(只登记)。
|
||||
b.process(context.Background(), nonOwnerGroupMsg("昔涟别刷屏了"))
|
||||
if judge.calls != 0 {
|
||||
t.Fatalf("judge calls = %d, want 0 (negative attitude → cooldown, no LLM)", judge.calls)
|
||||
@@ -263,20 +263,20 @@ func TestEngageColdAvoidCooldownSuppressesThenExpires(t *testing.T) {
|
||||
t.Fatalf("loop ran after negative attitude, session = %q", chat.sessionID)
|
||||
}
|
||||
|
||||
// During cooldown, even a role-name mention is NOT answered (no proactive reply).
|
||||
// 冷却期间,即使是角色名提及也不回复(不做主动回复)。
|
||||
b.process(context.Background(), nonOwnerGroupMsg("那聊聊昔涟"))
|
||||
if judge.calls != 0 || chat.sessionID != "" {
|
||||
t.Fatalf("during cooldown: calls=%d session=%q, want 0/\"\" (suppressed)", judge.calls, chat.sessionID)
|
||||
}
|
||||
|
||||
// A direct @ still breaks cooldown (design/18 §5: 除非 @ 她).
|
||||
// 直接 @ 仍会打断冷却(design/18 §5:除非 @ 她)。
|
||||
chat.sessionID = ""
|
||||
b.process(context.Background(), nonOwnerGroupMsg("在吗", adapter.Mention{UserID: "bot"}))
|
||||
if chat.sessionID != "sess-g" {
|
||||
t.Fatalf("mention during cooldown: session=%q, want sess-g (@ always replies)", chat.sessionID)
|
||||
}
|
||||
|
||||
// After the cooldown timer elapses, a role-name mention re-engages (PROBING → ACTIVE).
|
||||
// 冷却计时结束后,角色名提及会重新触发参与(PROBING → ACTIVE)。
|
||||
now = now.Add(2 * time.Hour)
|
||||
chat.sessionID = ""
|
||||
b.process(context.Background(), nonOwnerGroupMsg("再提昔涟"))
|
||||
@@ -295,7 +295,7 @@ func TestEngageColdAvoidFollowUpDuringActive(t *testing.T) {
|
||||
cfg.ActivityWindow = time.Hour
|
||||
cfg.ActivityMessages = 100
|
||||
judge := &engageScriptJudge{responses: []LightDecision{
|
||||
{Interesting: true, WorthReply: true, Reason: "感兴趣"}, // probe → ACTIVE
|
||||
{Interesting: true, WorthReply: true, Reason: "感兴趣"}, // 探测 → ACTIVE
|
||||
}}
|
||||
b, chat, _ := engageTestBridge(t, judge, cfg, dayNow(), nil)
|
||||
|
||||
@@ -304,7 +304,7 @@ func TestEngageColdAvoidFollowUpDuringActive(t *testing.T) {
|
||||
t.Fatalf("probe: session=%q, want sess-g", chat.sessionID)
|
||||
}
|
||||
|
||||
// A negative attitude during ACTIVE parks her in COOLDOWN immediately (design/18 §5).
|
||||
// ACTIVE 期间出现负面态度会立刻把她按进 COOLDOWN(design/18 §5)。
|
||||
b.process(context.Background(), nonOwnerGroupMsg("别说了太吵"))
|
||||
if judge.calls != 1 {
|
||||
t.Fatalf("judge calls = %d, want 1 (negative attitude must not consult the judge)", judge.calls)
|
||||
@@ -350,8 +350,8 @@ func TestEngageJudgeReceivesContextAndPersonaPrompt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// contextHistory is a captureHistory that returns a fixed recent window, so the
|
||||
// engagement context renderer can be asserted.
|
||||
// contextHistory 是返回固定近期窗口的 captureHistory,便于
|
||||
// 断言参与度上下文渲染器。
|
||||
type contextHistory struct {
|
||||
captureHistory
|
||||
recent []history.Message
|
||||
|
||||
Reference in new issue
Block a user