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

+28 -9
View File
@@ -19,15 +19,25 @@ import (
// marker fails safe to "no push".
var (
// decideMarker matches the push/no_push decision token, tolerating a colon in
// either width and optional spaces.
// either width and optional spaces. It is used to recover the decision, so it
// stays strict on the bracketed form: a bare "我决定:push" is treated as prose,
// not as a decision (fail safe).
decideMarker = regexp.MustCompile(`(?i)\[\s*我决定\s*[::]\s*(no_push|push)\s*\]`)
// channelMarker matches the target-channel token.
// channelMarker matches the target-channel token on the decision line.
channelMarker = regexp.MustCompile(`(?i)\[\s*渠道\s*[::]\s*([a-z0-9_\-]+)\s*\]`)
// decidePrefixes are the marker shapes that signal a decision was attempted
// but could not be parsed, so the pass is treated as ambiguous (fail safe). It
// is deliberately the bracketed form only, so a message that merely contains
// "我决定:" in prose (common in Chinese) is not misread as a malformed marker.
decidePrefixes = []string{"[我决定"}
// stripDecideRe / stripChannelRe recognize the decision/channel markers as a
// token to delete from the user-facing body, in their intended bracketed form
// and with full-width brackets, so a well-formatted decision never leaks.
stripDecideRe = regexp.MustCompile(`(?i)[【\[]\s*我决定\s*[::]\s*(no_push|push)\s*[】\]]`)
stripChannelRe = regexp.MustCompile(`(?i)[【\[]\s*渠道\s*[::]\s*[a-z0-9_\-]+\s*[】\]]`)
bareDecideRe = regexp.MustCompile(`(?i)我决定\s*[::]\s*(no_push|push)`)
bareChannelRe = regexp.MustCompile(`(?i)渠道\s*[::]\s*[a-z0-9_\-]+`)
)
// nonePhrases are conclusions that mean "nothing to report" when no marker is
@@ -65,7 +75,7 @@ func parseProactive(conclusion string) (message, channelHint string, push bool)
}
channelHint = matchChannel(text)
message = stripMarkers(text)
message = stripInternalMarkers(text)
if push && (message == "" || looksNone(message)) {
push = false
@@ -108,15 +118,24 @@ func matchChannel(text string) string {
return strings.ToLower(m[1])
}
// stripMarkers removes the structured decision/channel markers and any lines that
// become empty, leaving the clean message body. It tolerates the markers on their
// own lines or inline with the body.
func stripMarkers(text string) string {
// stripInternalMarkers removes the structured decision/channel markers so a
// proactive message body never carries them to the user. It handles the intended
// bracketed form ([我决定:push][渠道:kook]) plus the drift forms a model may emit —
// bare "我决定:push" / "渠道:kook" (no brackets) and full-width 【我决定:push】 — then
// drops any line that becomes empty and collapses the remainder. It is applied
// both inside parseProactive (so the message and record are clean) and again as a
// final guard in buildProactiveMessage, so a marker cannot survive into the
// outbound body no matter how it was formatted. Only our logs/records may carry
// the marker; the user-facing message must be pure warm content.
func stripInternalMarkers(text string) string {
lines := strings.Split(text, "\n")
out := make([]string, 0, len(lines))
for _, ln := range lines {
ln = strings.TrimSpace(decideMarker.ReplaceAllString(ln, ""))
ln = strings.TrimSpace(channelMarker.ReplaceAllString(ln, ""))
ln = stripDecideRe.ReplaceAllString(ln, "")
ln = stripChannelRe.ReplaceAllString(ln, "")
ln = bareDecideRe.ReplaceAllString(ln, "")
ln = bareChannelRe.ReplaceAllString(ln, "")
ln = strings.TrimSpace(ln)
if ln == "" {
continue
}