feat(engage): 群聊自主参与度引擎核心(design/18 P2a+P2b)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- persona 加 triggerWords(昔涟/Cyrene/有瑕的追忆/追忆);Loader 解析
- light.go: shouldRespond 拆分 legacy 与 engage 路径;LightInput/LightDecision 扩展
- engage.go(新): EngageConfig + IDLE/PROBING/ACTIVE/COOLDOWN 状态机(按 platform:groupID 内存) +
  概率触发(可注入rand) + 角色名提词触发 + 活动期跟随(话题转移/连续无感/窗口) + 负面态度冷避
- engagejudge.go(新): NewLLMLightJudge,一次成本受控轻LLM判 值不值得回(不判身份),
  解析失败/超时 fail-closed 静默
- channelbridge.go: Options(TriggerWords/PersonaPrompt/Engage/EngageFor/EngageRand) + 字段初始化
- wire.go: 接线 Light+TriggerWords+PersonaPrompt+Engage(默认概率0.10)
- 红线: 未触发/未命中概率绝不调LLM; 身份靠actor元数据; 敏感不落日志; 零新依赖
- 测试: engage_test.go(触发/状态机/冷避/成本受控/JUDGE输入) + persona triggerWords 用例
- 验收: go build/vet/test ./... 全绿
This commit is contained in:
Aska committed 2026-09-07 18:16:55 +08:00
1 parent 2d7a377d74
commit 8eaf81bd0d
9 files changed
+1156 -13

No files matched your search

+5
View File
@@ -86,6 +86,11 @@ type Persona struct {
Emotion EmotionParams `yaml:"emotion"`
Affection AffectionParams `yaml:"affection"`
Visual PersonaVisual `yaml:"visual,omitempty"`
// TriggerWords are the role-name words that, when a public-platform group
// message mentions one, route the message into the participation PROBING
// stage even without a direct @ (design/18 §2 触发②). Each persona defines its
// own reminder words; they never imply identity (that stays structural).
TriggerWords []string `yaml:"triggerWords,omitempty"`
}
// EmotionConfig converts this persona's own fields into the params consumed by
+18
View File
@@ -79,6 +79,24 @@ func TestLoad_ValidYAML(t *testing.T) {
}
}
func TestLoad_TriggerWords(t *testing.T) {
t.Parallel()
// A persona without triggerWords yields an empty slice; with them, the list is
// preserved (design/18 §2: per-persona role-name reminder words).
yaml := "id: xilian\nname: 昔涟\ncharacterRef: c\nidentity:\n role: r\n systemPrompt: s\n" +
"triggerWords:\n - 昔涟\n - Cyrene\n - 有瑕的追忆\n"
path := writeFile(t, t.TempDir(), "xilian.yaml", yaml)
p, err := Load(path)
if err != nil {
t.Fatalf("Load() error: %v", err)
}
if len(p.TriggerWords) != 3 || p.TriggerWords[0] != "昔涟" ||
p.TriggerWords[1] != "Cyrene" || p.TriggerWords[2] != "有瑕的追忆" {
t.Fatalf("TriggerWords = %v, want [昔涟 Cyrene 有瑕的追忆]", p.TriggerWords)
}
}
func TestLoad_MissingRequiredFields(t *testing.T) {
t.Parallel()