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:
2026-05-31 21:07:25 +08:00
parent a9c79d7887
commit b085e58031
7 changed files with 179 additions and 33 deletions
@@ -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 += "[卡片消息]"
}
}
}
}