fix(backthink): 主动推送降频(15min心跳+60min≤1次+推后30min冷却+刚聊过10min冷避) + 情绪约束(分类限频/活跃感知) + 剥离'我决定:push'内部标记
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

This commit is contained in:
Aska committed 2026-09-08 13:08:57 +08:00
1 parent 03d7050e1e
commit 12031bd13c
14 files changed
+662 -149

No files matched your search

@@ -307,3 +307,59 @@ func TestPusher_SpecificPlatform_NoIM_FallsBackToActive(t *testing.T) {
t.Fatalf("Detail = %q, want it to include the kook hint", res.Detail)
}
}
// TestStripInternalMarkers_CleanBody verifies the internal decision/channel
// markers are removed from a proactive body in every shape a model may emit
// (bracketed, full-width, bare), so a marker can never reach the user-facing text.
func TestStripInternalMarkers_CleanBody(t *testing.T) {
t.Parallel()
cases := []struct {
name, in, want string
}{
{name: "bracketed own lines", in: "[我决定:push]\n[渠道:kook]\n我想提醒你明天有体检", want: "我想提醒你明天有体检"},
{name: "inline bracketed", in: "[我决定:push] [渠道:WEB] 晚上早点睡", want: "晚上早点睡"},
{name: "bare markers", in: "我决定:push\n渠道:kook\n记得喝水哦", want: "记得喝水哦"},
{name: "full-width brackets", in: "【我决定:push】【渠道:telegram】早点休息", want: "早点休息"},
{name: "marker trailing body", in: "今天风很大[我决定:push][渠道:kook]", want: "今天风很大"},
{name: "no marker", in: "洗衣机洗好了,衣服可以收了", want: "洗衣机洗好了,衣服可以收了"},
{name: "empty", in: "", want: ""},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := stripInternalMarkers(tc.in); got != tc.want {
t.Fatalf("stripInternalMarkers(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
// TestThink_BareMarkersStrippedFromBody is the regression for the KOOK leak: a
// conclusion carrying bare (bracket-less) "我决定:push"/"渠道:..." markers must
// still deliver a clean warm body, never the structured decision text.
func TestThink_BareMarkersStrippedFromBody(t *testing.T) {
t.Parallel()
conclusion := "我决定:push\n渠道:kook\n记得喝水哦"
thinker := &mockThinker{text: conclusion}
im := &mockIMAdapter{}
b, _ := newBackthink(t, Config{}, Deps{
Thinker: thinker,
State: &mockState{state: UserState{}},
IM: im,
Gate: &mockGate{allow: true},
Now: func() time.Time { return dayAt() },
})
b.think(context.Background(), heartbeatTrigger())
if im.count() != 1 {
t.Fatalf("im calls = %d, want 1", im.count())
}
if im.last.Content != "记得喝水哦" {
t.Fatalf("im content = %q, want the clean body", im.last.Content)
}
if strings.Contains(im.last.Content, "我决定") || strings.Contains(im.last.Content, "渠道") {
t.Fatalf("internal marker leaked into the message body: %q", im.last.Content)
}
}