docs(comments): 注释中文化 · internal/services/channelbridge(纯替换)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避)
- 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
This commit is contained in:
AskaEth committed 2026-10-04 13:33:06 +08:00
1 parent 61cf80d5ee
commit 093f38d986
28 files changed
+1089 -1089

No files matched your search

+37 -37
View File
@@ -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