|
|
|
@@ -30,24 +30,59 @@ type PendingThought struct {
|
|
|
|
|
Consumed bool `json:"consumed"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ProactiveTarget describes the target for proactive platform messages (QQ, etc.).
|
|
|
|
|
// ProactiveTarget describes the target for proactive platform messages (OBv11, etc.).
|
|
|
|
|
// When nil, the message goes through the existing Web push path.
|
|
|
|
|
type ProactiveTarget struct {
|
|
|
|
|
Platform string // "qq"
|
|
|
|
|
ChatType string // "private" or "group"
|
|
|
|
|
UserID string // QQ user number
|
|
|
|
|
GroupID string // QQ group number (group chat)
|
|
|
|
|
AtUserID string // QQ number to @mention (optional)
|
|
|
|
|
UserID string // OBv11 user number
|
|
|
|
|
GroupID string // OBv11 group number (group chat)
|
|
|
|
|
AtUserID string // OBv11 number to @mention (optional)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PlatformChannel represents a platform channel to observe for background thinking.
|
|
|
|
|
type PlatformChannel struct {
|
|
|
|
|
Platform string // qq, telegram, etc.
|
|
|
|
|
ChannelType string // group, private
|
|
|
|
|
ChannelID string // group ID or user QQ number
|
|
|
|
|
ChannelID string // group ID or user OBv11 number
|
|
|
|
|
ChannelName string // group name or user display name (resolved at runtime)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PlatformFormat describes how proactive message targets are formatted for a platform.
|
|
|
|
|
type PlatformFormat struct {
|
|
|
|
|
// Human-readable platform label (e.g. "OBv11", "Telegram").
|
|
|
|
|
Label string
|
|
|
|
|
// Regex pattern with 3 capture groups: (chatType, channelID, atUserID).
|
|
|
|
|
// chatType group must contain the platform-specific chat type keyword (e.g. "群聊", "私聊").
|
|
|
|
|
TargetRegex string
|
|
|
|
|
// Compiled regex (populated at registration time).
|
|
|
|
|
compiled *regexp.Regexp
|
|
|
|
|
// Format verbs for building markers in prompts.
|
|
|
|
|
GroupMarkerFmt string // e.g. "【OBv11群聊:%s】"
|
|
|
|
|
PrivateMarkerFmt string // e.g. "【OBv11私聊:%s】"
|
|
|
|
|
GroupAtMarkerFmt string // e.g. "【OBv11群聊:%s@%s】"
|
|
|
|
|
// Chat type strings used by this platform in regex and markers.
|
|
|
|
|
GroupKeyword string // e.g. "群聊"
|
|
|
|
|
PrivateKeyword string // e.g. "私聊"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// defaultPlatformFormats returns the built-in platform format registry.
|
|
|
|
|
func defaultPlatformFormats() map[string]*PlatformFormat {
|
|
|
|
|
formats := make(map[string]*PlatformFormat)
|
|
|
|
|
qq := &PlatformFormat{
|
|
|
|
|
Label: "OBv11",
|
|
|
|
|
TargetRegex: `【OBv11(私聊|群聊):(\d+)(?:@(\d+))?】`,
|
|
|
|
|
GroupMarkerFmt: "【OBv11群聊:%s】",
|
|
|
|
|
PrivateMarkerFmt: "【OBv11私聊:%s】",
|
|
|
|
|
GroupAtMarkerFmt: "【OBv11群聊:%s@%s】",
|
|
|
|
|
GroupKeyword: "群聊",
|
|
|
|
|
PrivateKeyword: "私聊",
|
|
|
|
|
}
|
|
|
|
|
qq.compiled = regexp.MustCompile(qq.TargetRegex)
|
|
|
|
|
formats["qq"] = qq
|
|
|
|
|
return formats
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ParsePlatformChannels parses PLATFORM_CHANNELS env var.
|
|
|
|
|
// Format: "qq:group:123456,telegram:group:789012"
|
|
|
|
|
// Optional 4th field for display name: "qq:group:123456:群名称"
|
|
|
|
@@ -193,6 +228,9 @@ type Thinker struct {
|
|
|
|
|
|
|
|
|
|
// 平台 Bot UID (platform -> bot's own UID, e.g. "qq" -> "123456789")
|
|
|
|
|
botUIDs map[string]string
|
|
|
|
|
|
|
|
|
|
// 平台格式注册表 (platform -> format)
|
|
|
|
|
platformFormats map[string]*PlatformFormat
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AutonomousToolPolicy 自主思考工具调用安全策略
|
|
|
|
@@ -237,14 +275,14 @@ func (t *Thinker) SetMessagePusher(pusher func(string, string, string)) {
|
|
|
|
|
t.messagePusher = pusher
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetPlatformMessagePusher sets the callback for pushing proactive messages to platform adapters (QQ, etc.).
|
|
|
|
|
// SetPlatformMessagePusher sets the callback for pushing proactive messages to platform adapters (OBv11, etc.).
|
|
|
|
|
func (t *Thinker) SetPlatformMessagePusher(pusher func(ProactiveTarget, string)) {
|
|
|
|
|
t.mu.Lock()
|
|
|
|
|
defer t.mu.Unlock()
|
|
|
|
|
t.platformMessagePusher = pusher
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetBotUID sets the bot's own platform UID (e.g., QQ number).
|
|
|
|
|
// SetBotUID sets the bot's own platform UID (e.g., OBv11 account).
|
|
|
|
|
func (t *Thinker) SetBotUID(platform, uid string) {
|
|
|
|
|
t.mu.Lock()
|
|
|
|
|
defer t.mu.Unlock()
|
|
|
|
@@ -412,6 +450,7 @@ func NewThinker(
|
|
|
|
|
proactiveGuard: DefaultProactiveGuard(),
|
|
|
|
|
platformChannels: cfg.PlatformChannels,
|
|
|
|
|
platformThinkInterval: cfg.PlatformSilentThinkInterval,
|
|
|
|
|
platformFormats: defaultPlatformFormats(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1481,7 +1520,7 @@ func (t *Thinker) buildThinkingUserPrompt(
|
|
|
|
|
sb.WriteString("\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// QQ platform identity and available channels for proactive messaging.
|
|
|
|
|
// OBv11 platform identity and available channels for proactive messaging.
|
|
|
|
|
t.mu.Lock()
|
|
|
|
|
qqChannels := t.platformChannels
|
|
|
|
|
botUIDs := t.botUIDs
|
|
|
|
@@ -1489,11 +1528,11 @@ func (t *Thinker) buildThinkingUserPrompt(
|
|
|
|
|
lastMsgTime := t.lastUserMessage
|
|
|
|
|
t.mu.Unlock()
|
|
|
|
|
if len(qqChannels) > 0 {
|
|
|
|
|
sb.WriteString("\n\n【你的QQ平台身份与可用频道】\n")
|
|
|
|
|
sb.WriteString("\n\n【你的平台身份与可用频道】\n")
|
|
|
|
|
|
|
|
|
|
// Bot's own identity.
|
|
|
|
|
if qqBotUID, ok := botUIDs["qq"]; ok && qqBotUID != "" {
|
|
|
|
|
sb.WriteString(fmt.Sprintf("你在QQ上的账号是: %s(昔涟),这就是你。\n", qqBotUID))
|
|
|
|
|
sb.WriteString(fmt.Sprintf("你在OBv11上的账号是: %s(昔涟),这就是你。\n", qqBotUID))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Active session context with time, group name, and trigger-aware guidance.
|
|
|
|
@@ -1518,23 +1557,23 @@ func (t *Thinker) buildThinkingUserPrompt(
|
|
|
|
|
if ch.ChannelType == "group" {
|
|
|
|
|
if triggerReason == "post_chat" {
|
|
|
|
|
if timeHint != "" && timeHint != "刚刚" {
|
|
|
|
|
sb.WriteString(fmt.Sprintf("【%s】你当前正在QQ群聊 %s 中。刚刚群里有人说了话——如果你想回应,用【主动消息】【QQ群聊:%s】格式输出。\n", timeHint, chLabel, activeChID))
|
|
|
|
|
sb.WriteString(fmt.Sprintf("【%s】你当前正在OBv11群聊 %s 中。刚刚群里有人说了话——如果你想回应,用【主动消息】【OBv11群聊:%s】格式输出。\n", timeHint, chLabel, activeChID))
|
|
|
|
|
} else {
|
|
|
|
|
sb.WriteString(fmt.Sprintf("【刚刚】你当前正在QQ群聊 %s 中。群里有人说了话——如果你想回应,用【主动消息】【QQ群聊:%s】格式输出。\n", chLabel, activeChID))
|
|
|
|
|
sb.WriteString(fmt.Sprintf("【刚刚】你当前正在OBv11群聊 %s 中。群里有人说了话——如果你想回应,用【主动消息】【OBv11群聊:%s】格式输出。\n", chLabel, activeChID))
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Autonomous thinking (periodic, silence, startup).
|
|
|
|
|
if timeHint != "" {
|
|
|
|
|
sb.WriteString(fmt.Sprintf("【上次活跃: %s】你当前在QQ群聊 %s 中。现在是自主思考时间——如果你想主动对群里说些什么,用【主动消息】【QQ群聊:%s】格式。\n", timeHint, chLabel, activeChID))
|
|
|
|
|
sb.WriteString(fmt.Sprintf("【上次活跃: %s】你当前在OBv11群聊 %s 中。现在是自主思考时间——如果你想主动对群里说些什么,用【主动消息】【OBv11群聊:%s】格式。\n", timeHint, chLabel, activeChID))
|
|
|
|
|
} else {
|
|
|
|
|
sb.WriteString(fmt.Sprintf("你当前在QQ群聊 %s 中。现在是自主思考时间——如果你想主动对群里说些什么,用【主动消息】【QQ群聊:%s】格式。\n", chLabel, activeChID))
|
|
|
|
|
sb.WriteString(fmt.Sprintf("你当前在OBv11群聊 %s 中。现在是自主思考时间——如果你想主动对群里说些什么,用【主动消息】【OBv11群聊:%s】格式。\n", chLabel, activeChID))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if triggerReason == "post_chat" {
|
|
|
|
|
sb.WriteString(fmt.Sprintf("【刚刚】你当前正在与QQ用户 %s 私聊。想回应ta的话用【主动消息】【QQ私聊:%s】格式。\n", chLabel, activeChID))
|
|
|
|
|
sb.WriteString(fmt.Sprintf("【刚刚】你当前正在与OBv11用户 %s 私聊。想回应ta的话用【主动消息】【OBv11私聊:%s】格式。\n", chLabel, activeChID))
|
|
|
|
|
} else {
|
|
|
|
|
sb.WriteString(fmt.Sprintf("你当前在与QQ用户 %s 私聊。现在是自主思考时间——想主动发消息用【主动消息】【QQ私聊:%s】格式。\n", chLabel, activeChID))
|
|
|
|
|
sb.WriteString(fmt.Sprintf("你当前在与OBv11用户 %s 私聊。现在是自主思考时间——想主动发消息用【主动消息】【OBv11私聊:%s】格式。\n", chLabel, activeChID))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
@@ -1543,10 +1582,10 @@ func (t *Thinker) buildThinkingUserPrompt(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sb.WriteString("\n发送主动消息的格式:\n")
|
|
|
|
|
sb.WriteString("- QQ私聊: 【主动消息】【QQ私聊:QQ号】消息内容\n")
|
|
|
|
|
sb.WriteString("- QQ群聊: 【主动消息】【QQ群聊:群号】消息内容\n")
|
|
|
|
|
sb.WriteString("- QQ群聊@某人: 【主动消息】【QQ群聊:群号@QQ号】消息内容\n")
|
|
|
|
|
sb.WriteString("\n可用的QQ频道列表:\n")
|
|
|
|
|
sb.WriteString("- OBv11私聊: 【主动消息】【OBv11私聊:账号】消息内容\n")
|
|
|
|
|
sb.WriteString("- OBv11群聊: 【主动消息】【OBv11群聊:群号】消息内容\n")
|
|
|
|
|
sb.WriteString("- OBv11群聊@某人: 【主动消息】【OBv11群聊:群号@账号】消息内容\n")
|
|
|
|
|
sb.WriteString("\n可用的平台频道列表:\n")
|
|
|
|
|
for _, ch := range qqChannels {
|
|
|
|
|
if ch.Platform != "qq" {
|
|
|
|
|
continue
|
|
|
|
@@ -1567,7 +1606,7 @@ func (t *Thinker) buildThinkingUserPrompt(
|
|
|
|
|
sb.WriteString("- 群聊消息要有公共价值,不要当成私聊\n")
|
|
|
|
|
sb.WriteString("- @某人时:确认这个人在群里有发言过,你的@内容是对他之前说的话的回应\n")
|
|
|
|
|
if triggerReason == "post_chat" {
|
|
|
|
|
sb.WriteString("- 重要:如果你在反思中想往QQ群发消息,必须用【主动消息】【QQ群聊:群号】格式,不要省略群号!\n")
|
|
|
|
|
sb.WriteString("- 重要:如果你在反思中想往平台群发消息,必须用【主动消息】【OBv11群聊:群号】格式,不要省略群号!\n")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1639,8 +1678,8 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
|
|
|
|
|
t.pendingThoughts = t.pendingThoughts[len(t.pendingThoughts)-10:]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract proactive message and optional QQ target.
|
|
|
|
|
proactiveMsg, proactiveTarget := extractProactiveMessage(content)
|
|
|
|
|
// Extract proactive message and optional platform target.
|
|
|
|
|
proactiveMsg, proactiveTarget := t.extractProactiveMessage(content)
|
|
|
|
|
// Prefer active session, fall back to admin main session.
|
|
|
|
|
pushSessionID := t.activeSessionID
|
|
|
|
|
if pushSessionID == "" {
|
|
|
|
@@ -1729,13 +1768,10 @@ func (t *Thinker) storeThought(content string, toolCallsJSON string, toolCallCou
|
|
|
|
|
//
|
|
|
|
|
// 要求标记独立成行(前面只有空白或行首),避免把自然语言中的提及
|
|
|
|
|
// 当作指令(如 "不需要写【主动消息】" 这类否定表述)。
|
|
|
|
|
// qqTargetRe matches QQ target markers like 【QQ私聊:123456】, 【QQ群聊:789012】, 【QQ群聊:789012@123456】
|
|
|
|
|
var qqTargetRe = regexp.MustCompile(`【QQ(私聊|群聊):(\d+)(?:@(\d+))?】`)
|
|
|
|
|
|
|
|
|
|
// extractProactiveMessage extracts the 【主动消息】 marker and optional QQ target from thinking content.
|
|
|
|
|
// extractProactiveMessage extracts the 【主动消息】 marker and optional platform target from thinking content.
|
|
|
|
|
// Returns the message content and an optional ProactiveTarget for platform delivery.
|
|
|
|
|
// When target is nil, the message goes through the existing Web push path.
|
|
|
|
|
func extractProactiveMessage(content string) (string, *ProactiveTarget) {
|
|
|
|
|
func (t *Thinker) extractProactiveMessage(content string) (string, *ProactiveTarget) {
|
|
|
|
|
marker := "【主动消息】"
|
|
|
|
|
|
|
|
|
|
// Scan each line; only accept lines where the marker starts the line (ignoring leading whitespace).
|
|
|
|
@@ -1756,33 +1792,38 @@ func extractProactiveMessage(content string) (string, *ProactiveTarget) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse optional QQ target marker right after 【主动消息】.
|
|
|
|
|
// Parse optional platform target marker right after 【主动消息】.
|
|
|
|
|
msg := raw
|
|
|
|
|
var target *ProactiveTarget
|
|
|
|
|
if loc := qqTargetRe.FindStringSubmatchIndex(raw); loc != nil && loc[0] == 0 {
|
|
|
|
|
m := qqTargetRe.FindStringSubmatch(raw)
|
|
|
|
|
if len(m) == 4 {
|
|
|
|
|
chatType := "private"
|
|
|
|
|
if m[1] == "群聊" {
|
|
|
|
|
chatType = "group"
|
|
|
|
|
}
|
|
|
|
|
target = &ProactiveTarget{
|
|
|
|
|
Platform: "qq",
|
|
|
|
|
ChatType: chatType,
|
|
|
|
|
UserID: m[2],
|
|
|
|
|
GroupID: m[2],
|
|
|
|
|
AtUserID: m[3],
|
|
|
|
|
}
|
|
|
|
|
// For private chat, UserID is the QQ number; GroupID stays empty.
|
|
|
|
|
if chatType == "private" {
|
|
|
|
|
target.GroupID = ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Remove the QQ target marker from the message content.
|
|
|
|
|
msg = strings.TrimSpace(raw[loc[1]:])
|
|
|
|
|
if msg == "" {
|
|
|
|
|
// Try each registered platform format.
|
|
|
|
|
for platform, pf := range t.platformFormats {
|
|
|
|
|
if pf.compiled == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if loc := pf.compiled.FindStringSubmatchIndex(raw); loc != nil && loc[0] == 0 {
|
|
|
|
|
m := pf.compiled.FindStringSubmatch(raw)
|
|
|
|
|
if len(m) == 4 {
|
|
|
|
|
chatType := "private"
|
|
|
|
|
if m[1] == pf.GroupKeyword {
|
|
|
|
|
chatType = "group"
|
|
|
|
|
}
|
|
|
|
|
target = &ProactiveTarget{
|
|
|
|
|
Platform: platform,
|
|
|
|
|
ChatType: chatType,
|
|
|
|
|
UserID: m[2],
|
|
|
|
|
GroupID: m[2],
|
|
|
|
|
AtUserID: m[3],
|
|
|
|
|
}
|
|
|
|
|
if chatType == "private" {
|
|
|
|
|
target.GroupID = ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
msg = strings.TrimSpace(raw[loc[1]:])
|
|
|
|
|
if msg == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Limit message length (200 chars max, keep it short).
|
|
|
|
|