feat(backthink): 静默时段心跳降频(A)—— THINK_QUIET_INTERVAL_SEC 默认 3600

主人裁定「先做 A(静默降频)再转向 C」;背景实测:静默期**思考频率不变**(恒定 900s 心跳),只拦打扰 ⇒ 成本照付。
- 新键 THINK_QUIET_INTERVAL_SEC(默认 3600):窗口内用该间隔、窗外仍用 THINK_INTERVAL_SEC;**≤0 = 关闭降频**(不许出现「半夜完全不思考」暗坑);窗口关闭/钟点非法 ⇒ 不降频
- 心跳**每拍按时刻选间隔**(放到循环外就永远不生效——变异实测能打红)
- 🔴 不变量:**事件驱动不被降频影响**(TestBackthink_EventTrigger_NotSlowedByQuietInterval 钉住)
- 事实卡 9→12 条(+思考总开关/普通间隔/静默间隔,标「改需重启」);契约形状未变、未升版
- ⚠️ 如实登记:ENABLE_BACKGROUND_THINKING 是**死配置**(全仓无消费者),已在 env/面板标签/契约三处标注,**是否接线待主人拍**
- 变异 5 条全红;C 的缺口清单(唤醒面 7 处)已回给主会话排期
This commit is contained in:
AskaEth committed 2026-10-05 22:38:17 +08:00
1 parent d6ed9cfa2b
commit 236b202fda
13 files changed
+633 -54

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
package config
import (
"strings"
"testing"
)
// TestLoad_BackgroundThinkingDefaults 锁定后台思考心跳(design/12 §N A1)的 env
// 契约默认值:普通心跳 900s、安静窗口内心跳 3600s。后者必须与设计稿一致(1 小时),
// 否则「没配这个键」与「配了但解析失败」会走向不同的行为。
func TestLoad_BackgroundThinkingDefaults(t *testing.T) {
t.Setenv("JWT_SECRET", strings.Repeat("s", 40))
cfg, err := Load("")
if err != nil {
t.Fatalf("Load() error: %v", err)
}
got := cfg.Background
if !got.Enabled {
t.Error("ENABLE_BACKGROUND_THINKING 默认应为 true")
}
if got.IntervalSec != 900 {
t.Errorf("THINK_INTERVAL_SEC = %d, want 900", got.IntervalSec)
}
if got.QuietIntervalSec != 3600 {
t.Errorf("THINK_QUIET_INTERVAL_SEC = %d, want 3600(design/12 §N A1)", got.QuietIntervalSec)
}
if got.QuietStartHour != 23 || got.QuietEndHour != 7 {
t.Errorf("安静时段 = %d-%d, want 23-7", got.QuietStartHour, got.QuietEndHour)
}
}
// TestLoad_BackgroundThinkingEnvOverrides 锁定键名与类型:写进 .env 的旋钮真的生效
// (键名写错会静默沿用默认值,这类错误只能靠这条用例抓住)。
// `THINK_QUIET_INTERVAL_SEC=0` 是**合法值**(关闭降频,design/12 §N A3),因此它必须
// 原样解析成 0,不能被默认值覆盖。
func TestLoad_BackgroundThinkingEnvOverrides(t *testing.T) {
t.Setenv("JWT_SECRET", strings.Repeat("s", 40))
t.Setenv("THINK_INTERVAL_SEC", "1200")
t.Setenv("THINK_QUIET_INTERVAL_SEC", "0")
cfg, err := Load("")
if err != nil {
t.Fatalf("Load() error: %v", err)
}
if cfg.Background.IntervalSec != 1200 {
t.Fatalf("THINK_INTERVAL_SEC 未生效:%d", cfg.Background.IntervalSec)
}
if cfg.Background.QuietIntervalSec != 0 {
t.Fatalf("THINK_QUIET_INTERVAL_SEC=0 必须原样解析成 0(关闭降频),得到 %d",
cfg.Background.QuietIntervalSec)
}
}
+19 -11
View File
@@ -216,18 +216,26 @@ type VoiceConfig struct {
// single source shared by the backthink service and the channel bridge's
// minimal-disturbance quiet-hours gate (design/12 §6 策略五). Equal non-zero hours
// disable the window. The Push*/Kind fields tune the proactive-push frequency of
// design/11 §6 + design/12 §6 and are mapped into backthink.Config.
// design/11 §6 + design/12 §6 and are mapped into backthink.Config. QuietIntervalSec
// 是安静窗口内的心跳周期(design/12 §N A1):窗口只降低**周期性心跳**的频率,
// 事件驱动通路完全不受影响(§N A2)。
type BackgroundConfig struct {
Enabled bool `env:"ENABLE_BACKGROUND_THINKING" default:"true"`
IntervalSec int `env:"THINK_INTERVAL_SEC" default:"900"`
SilenceTimeout int `env:"THINK_SILENCE_TIMEOUT_SEC" default:"120"`
QuietStartHour int `env:"QUIET_START_HOUR" default:"23"`
QuietEndHour int `env:"QUIET_END_HOUR" default:"7"`
PushLimit int `env:"THINK_PUSH_LIMIT" default:"1"`
PushWindowSec int `env:"THINK_PUSH_WINDOW_SEC" default:"3600"`
PushCooldownSec int `env:"THINK_PUSH_COOLDOWN_SEC" default:"1800"`
RecentChatCooldownSec int `env:"THINK_RECENT_CHAT_COOLDOWN_SEC" default:"600"`
KindWindowSec int `env:"THINK_KIND_WINDOW_SEC" default:"7200"`
Enabled bool `env:"ENABLE_BACKGROUND_THINKING" default:"true"`
IntervalSec int `env:"THINK_INTERVAL_SEC" default:"900"`
// QuietIntervalSec 是**安静窗口内**的心跳周期(秒),默认 3600(1 小时):
// 当前时刻落在安静时段(QuietStartHour–QuietEndHour)内时,心跳改用该间隔降频,
// 窗外仍用 IntervalSec(design/12 §N A1)。**≤0 = 关闭降频**(回落 IntervalSec,
// 语义唯一真源 = internal/services/backthink/heartbeat.go 的 heartbeatInterval);
// 它不是「半夜完全不思考」的开关,见 design/12 §N A3。
QuietIntervalSec int `env:"THINK_QUIET_INTERVAL_SEC" default:"3600"`
SilenceTimeout int `env:"THINK_SILENCE_TIMEOUT_SEC" default:"120"`
QuietStartHour int `env:"QUIET_START_HOUR" default:"23"`
QuietEndHour int `env:"QUIET_END_HOUR" default:"7"`
PushLimit int `env:"THINK_PUSH_LIMIT" default:"1"`
PushWindowSec int `env:"THINK_PUSH_WINDOW_SEC" default:"3600"`
PushCooldownSec int `env:"THINK_PUSH_COOLDOWN_SEC" default:"1800"`
RecentChatCooldownSec int `env:"THINK_RECENT_CHAT_COOLDOWN_SEC" default:"600"`
KindWindowSec int `env:"THINK_KIND_WINDOW_SEC" default:"7200"`
}
// MemoryTidyConfig 是记忆整理(design/23-记忆整理与去重)的开关与配额旋钮。