fix: 后台思考身份混淆 + 静默模式视觉理解 + QQ卡片解析 + 仪表盘状态修复
- 后台思考对话历史增加标签说明,严格区分群聊中不同发送者 - 静默观察模式传入图片URL并预处理,供后台思考参考 - 视觉+OCR双模型结果合并格式优化,避免LLM误认为多张图片 - QQ卡片消息(CQ:json)正确解析标题/类型,不再丢失为[JSON] - 进程管理器stop()在进程为null时重置pid/startTime,消除矛盾状态 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -180,7 +180,7 @@ func main() {
|
||||
case isAdmin && !isBotMentioned && shouldAdminBeSilent(msg, router):
|
||||
msg.RouteType = "silent"
|
||||
namespace := buildMemoryNamespace(msg.Platform, msg.ChannelType, msg.ChannelID)
|
||||
response, routeErr = forwardToAICore(cfg, msg, "platform_silent", namespace, namespace, nil, videoURLs, voiceURLs, isAdmin)
|
||||
response, routeErr = forwardToAICore(cfg, msg, "platform_silent", namespace, namespace, imageURLs, videoURLs, voiceURLs, isAdmin)
|
||||
|
||||
case isAdmin:
|
||||
msg.RouteType = "normal"
|
||||
@@ -195,12 +195,12 @@ func main() {
|
||||
// the admin already gets QQ's native @notification. Observe silently.
|
||||
msg.RouteType = "silent"
|
||||
namespace := buildMemoryNamespace(msg.Platform, msg.ChannelType, msg.ChannelID)
|
||||
response, routeErr = forwardToAICore(cfg, msg, "platform_silent", namespace, namespace, nil, videoURLs, voiceURLs, isAdmin)
|
||||
response, routeErr = forwardToAICore(cfg, msg, "platform_silent", namespace, namespace, imageURLs, videoURLs, voiceURLs, isAdmin)
|
||||
|
||||
case isSilent:
|
||||
msg.RouteType = "silent"
|
||||
namespace := buildMemoryNamespace(msg.Platform, msg.ChannelType, msg.ChannelID)
|
||||
silentResponse, silentErr := forwardToAICore(cfg, msg, "platform_silent", namespace, namespace, nil, videoURLs, voiceURLs, isAdmin)
|
||||
silentResponse, silentErr := forwardToAICore(cfg, msg, "platform_silent", namespace, namespace, imageURLs, videoURLs, voiceURLs, isAdmin)
|
||||
if silentErr != nil {
|
||||
msgLogger.Log(logging.LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
@@ -769,23 +769,49 @@ func parseSSEAndAccumulate(body string) string {
|
||||
return strings.Join(deltas, "")
|
||||
}
|
||||
|
||||
// splitContent splits text by ♪ (sentence-break marker), then by \n\n within each segment.
|
||||
// Non-empty segments are each wrapped as a chat message; empty input returns a single empty message.
|
||||
// splitContent splits text into separate chat messages.
|
||||
// It first splits by \n\n (message separator), then within each message
|
||||
// optionally splits further by ♪ (sentence-break marker).
|
||||
// Very short segments (< 10 chars) are merged with their neighbors to avoid
|
||||
// one-word messages followed by a wall of text.
|
||||
func splitContent(text string) []bridge.ResponseMessage {
|
||||
// First split by ♪ sentence-break marker.
|
||||
var rawParts []string
|
||||
if strings.Contains(text, "♪") {
|
||||
rawParts = strings.Split(text, "♪")
|
||||
} else {
|
||||
rawParts = strings.Split(text, "\n\n")
|
||||
}
|
||||
// Step 1: split by \n\n (message-level separator) always.
|
||||
rawParts := strings.Split(text, "\n\n")
|
||||
var parts []string
|
||||
for _, p := range rawParts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
// Step 2: within each \n\n segment, split by ♪ if present.
|
||||
if strings.Contains(p, "♪") {
|
||||
for _, sub := range strings.Split(p, "♪") {
|
||||
sub = strings.TrimSpace(sub)
|
||||
if sub != "" {
|
||||
parts = append(parts, sub)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parts = append(parts, p)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: merge very short segments with neighbors.
|
||||
const minRunes = 8
|
||||
var merged []string
|
||||
for _, part := range parts {
|
||||
if len(merged) > 0 && len([]rune(merged[len(merged)-1])) < minRunes {
|
||||
// Previous segment is too short: merge current into it.
|
||||
merged[len(merged)-1] = merged[len(merged)-1] + "\n" + part
|
||||
} else if len(merged) > 0 && len([]rune(part)) < minRunes {
|
||||
// Current segment is too short: merge it into previous.
|
||||
merged[len(merged)-1] = merged[len(merged)-1] + "\n" + part
|
||||
} else {
|
||||
merged = append(merged, part)
|
||||
}
|
||||
}
|
||||
parts = merged
|
||||
|
||||
var msgs []bridge.ResponseMessage
|
||||
for _, part := range parts {
|
||||
msgs = append(msgs, bridge.ResponseMessage{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -506,6 +507,55 @@ func (a *Adapter) ReadMessages(ctx context.Context, msgCh chan<- *OBv11Message)
|
||||
}
|
||||
}
|
||||
|
||||
// parseJSONCardTitle extracts a human-readable title from a QQ JSON card.
|
||||
// The data is the raw JSON string from the "data" field of a json-type message segment.
|
||||
func parseJSONCardTitle(data string) string {
|
||||
var card struct {
|
||||
App string `json:"app"`
|
||||
Prompt string `json:"prompt"`
|
||||
Title string `json:"title"`
|
||||
Desc string `json:"desc"`
|
||||
Meta struct {
|
||||
Detail1 struct {
|
||||
Title string `json:"title"`
|
||||
Desc string `json:"desc"`
|
||||
} `json:"detail_1"`
|
||||
News struct {
|
||||
Title string `json:"title"`
|
||||
Desc string `json:"desc"`
|
||||
} `json:"news"`
|
||||
Music struct {
|
||||
Title string `json:"title"`
|
||||
Desc string `json:"desc"`
|
||||
} `json:"music"`
|
||||
} `json:"meta"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(data), &card); err != nil {
|
||||
return "[卡片消息]"
|
||||
}
|
||||
|
||||
// Prefer prompt (e.g. "[分享]标题"), then meta titles, then top-level title.
|
||||
if card.Prompt != "" {
|
||||
return "[卡片] " + card.Prompt
|
||||
}
|
||||
if card.Meta.Detail1.Title != "" {
|
||||
return "[卡片] " + card.Meta.Detail1.Title
|
||||
}
|
||||
if card.Meta.News.Title != "" {
|
||||
return "[卡片] " + card.Meta.News.Title
|
||||
}
|
||||
if card.Meta.Music.Title != "" {
|
||||
return "[卡片] " + card.Meta.Music.Title
|
||||
}
|
||||
if card.Title != "" {
|
||||
return "[卡片] " + card.Title
|
||||
}
|
||||
if card.Desc != "" {
|
||||
return "[卡片] " + card.Desc
|
||||
}
|
||||
return "[卡片消息]"
|
||||
}
|
||||
|
||||
// cqSimplifyMap maps CQ code types to simplified Chinese labels.
|
||||
var cqSimplifyMap = map[string]string{
|
||||
"image": "[图片]",
|
||||
@@ -528,6 +578,15 @@ func simplifyCQCodes(s string) string {
|
||||
break
|
||||
}
|
||||
}
|
||||
if typ == "json" {
|
||||
// Parse data= field from [CQ:json,data=URL_ENCODED_JSON]
|
||||
if dataVal := extractCQParam(match, "data"); dataVal != "" {
|
||||
if decoded, err := url.QueryUnescape(dataVal); err == nil {
|
||||
return parseJSONCardTitle(decoded)
|
||||
}
|
||||
}
|
||||
return "[卡片消息]"
|
||||
}
|
||||
if label, ok := cqSimplifyMap[typ]; ok {
|
||||
return label
|
||||
}
|
||||
@@ -535,6 +594,24 @@ func simplifyCQCodes(s string) string {
|
||||
})
|
||||
}
|
||||
|
||||
// extractCQParam extracts a named parameter value from a CQ code string.
|
||||
// e.g. extractCQParam("[CQ:json,data=hello%20world]", "data") → "hello%20world"
|
||||
func extractCQParam(cqCode, paramName string) string {
|
||||
prefix := paramName + "="
|
||||
idx := strings.Index(cqCode, prefix)
|
||||
if idx < 0 {
|
||||
return ""
|
||||
}
|
||||
val := cqCode[idx+len(prefix):]
|
||||
// Value ends at "," or "]"
|
||||
for i, c := range val {
|
||||
if c == ',' || c == ']' {
|
||||
return val[:i]
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// extractText retrieves plain text from an OBv11 message.
|
||||
// CQ codes are converted to human-readable form where applicable (e.g. [CQ:at,qq=xxx] → @xxx).
|
||||
func extractText(msg *OBv11Message) string {
|
||||
@@ -576,6 +653,16 @@ func extractText(msg *OBv11Message) string {
|
||||
case "reply":
|
||||
// Reply is handled separately in ToUnified with reply text.
|
||||
text += "[回复]"
|
||||
case "json":
|
||||
if data, ok := s["data"].(map[string]interface{}); ok {
|
||||
if inner, ok := data["data"].(string); ok && inner != "" {
|
||||
text += parseJSONCardTitle(inner)
|
||||
} else {
|
||||
text += "[卡片消息]"
|
||||
}
|
||||
} else {
|
||||
text += "[卡片消息]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user