fix: 消息日志增强 + 历史消息抑制 + SSE实时追踪 + 群聊上下文优化
- 日志:收/发消息均显示群名称,管理员显示真实QQ昵称而非"开拓者" - 历史消息:服务重启后NapCat回放的历史消息不再触发回复,静默注入上下文 - 消息时间戳:转发给AI时附带【消息时间: HH:MM:SS (XmXs前)】标记 - ♪ 分割符:QQ消息支持♪作为句子断点 - AI-Core SSE端点:全链路追踪实时推送,ethend不再5秒轮询 - 群聊上下文:AI-Core明确被告知消息来自群聊,以实际发送者为主语 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -34,8 +34,12 @@ type Adapter struct {
|
||||
conn *websocket.Conn
|
||||
connMu sync.Mutex
|
||||
connected bool
|
||||
connectedAt time.Time // when the connection was established (for historical message detection)
|
||||
srv *http.Server // HTTP server for WS upgrades (server mode only)
|
||||
|
||||
groupNames map[int64]string // group ID → group name cache
|
||||
groupNamesMu sync.RWMutex
|
||||
|
||||
pendingResponses map[string]chan *OBv11APIResponse
|
||||
respMu sync.Mutex
|
||||
}
|
||||
@@ -52,6 +56,7 @@ func NewAdapter(configName, mode, port, accessToken, remoteURL string, sendInter
|
||||
remoteURL: remoteURL,
|
||||
sendIntervalMs: sendIntervalMs,
|
||||
pendingResponses: make(map[string]chan *OBv11APIResponse),
|
||||
groupNames: make(map[int64]string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +65,76 @@ func (a *Adapter) ConfigName() string { return a.configName }
|
||||
func (a *Adapter) SendIntervalMs() int { return a.sendIntervalMs }
|
||||
func (a *Adapter) SelfID() string { return a.selfID }
|
||||
|
||||
// ConnectedAt returns the time the connection was established, for historical message detection.
|
||||
func (a *Adapter) ConnectedAt() time.Time {
|
||||
a.connMu.Lock()
|
||||
defer a.connMu.Unlock()
|
||||
return a.connectedAt
|
||||
}
|
||||
|
||||
// GroupName resolves a group ID to its name. Returns empty string if unknown.
|
||||
func (a *Adapter) GroupName(groupID int64) string {
|
||||
a.groupNamesMu.RLock()
|
||||
n, ok := a.groupNames[groupID]
|
||||
a.groupNamesMu.RUnlock()
|
||||
if ok {
|
||||
return n
|
||||
}
|
||||
// Try to resolve via API.
|
||||
a.fetchGroupName(groupID)
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetGroupName caches a group name for a group ID.
|
||||
func (a *Adapter) SetGroupName(groupID int64, name string) {
|
||||
a.groupNamesMu.Lock()
|
||||
a.groupNames[groupID] = name
|
||||
a.groupNamesMu.Unlock()
|
||||
}
|
||||
|
||||
// fetchGroupName tries to resolve a group name via NapCat HTTP API (client mode).
|
||||
func (a *Adapter) fetchGroupName(groupID int64) {
|
||||
if a.mode != "client" || a.remoteURL == "" {
|
||||
return
|
||||
}
|
||||
// Derive HTTP base from WS URL: ws://host:port → http://host:port
|
||||
httpBase := strings.Replace(a.remoteURL, "ws://", "http://", 1)
|
||||
httpBase = strings.Replace(httpBase, "wss://", "https://", 1)
|
||||
// Strip path suffix if present
|
||||
if idx := strings.LastIndex(httpBase, "/"); idx > 8 {
|
||||
httpBase = httpBase[:idx]
|
||||
}
|
||||
|
||||
go func() {
|
||||
url := fmt.Sprintf("%s/get_group_info?group_id=%d", httpBase, groupID)
|
||||
if a.accessToken != "" {
|
||||
url += "&access_token=" + a.accessToken
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var result struct {
|
||||
Data struct {
|
||||
GroupName string `json:"group_name"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return
|
||||
}
|
||||
if result.Data.GroupName != "" {
|
||||
a.SetGroupName(groupID, result.Data.GroupName)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *Adapter) Capabilities() bridge.PlatformCapabilities {
|
||||
return bridge.PlatformCapabilities{
|
||||
MaxMessageLength: 200,
|
||||
@@ -109,6 +184,7 @@ func (a *Adapter) wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
a.conn = conn
|
||||
a.connected = true
|
||||
a.connectedAt = time.Now()
|
||||
a.connMu.Unlock()
|
||||
fmt.Println("[qq] NapCat/OneBot connected (正向WS)")
|
||||
}
|
||||
@@ -148,6 +224,7 @@ func (a *Adapter) connectClient(ctx context.Context) error {
|
||||
}
|
||||
a.conn = conn
|
||||
a.connected = true
|
||||
a.connectedAt = time.Now()
|
||||
a.connMu.Unlock()
|
||||
|
||||
fmt.Printf("[qq] connected to NapCat OneBot WS (client mode): %s\n", a.remoteURL)
|
||||
@@ -257,6 +334,26 @@ func (a *Adapter) ToUnified(rawMessage interface{}) (*bridge.UnifiedMessage, err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: parse CQ at codes from string format (e.g. [CQ:at,qq=2254389756]).
|
||||
if len(mentions) == 0 {
|
||||
raw := msg.RawMessage
|
||||
if raw == "" {
|
||||
if s, ok := msg.Message.(string); ok {
|
||||
raw = s
|
||||
}
|
||||
}
|
||||
for _, m := range cqAtRegex.FindAllStringSubmatch(raw, -1) {
|
||||
if len(m) >= 2 {
|
||||
mentions = append(mentions, m[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve group name for group messages.
|
||||
groupName := ""
|
||||
if msg.MessageType == "group" {
|
||||
groupName = a.GroupName(msg.GroupID)
|
||||
}
|
||||
|
||||
attachments := extractAttachments(msg)
|
||||
|
||||
@@ -273,6 +370,7 @@ func (a *Adapter) ToUnified(rawMessage interface{}) (*bridge.UnifiedMessage, err
|
||||
Attachments: attachments,
|
||||
RawData: rawMessage,
|
||||
Timestamp: time.Unix(msg.Time, 0),
|
||||
GroupName: groupName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -416,6 +514,7 @@ func extractText(msg *OBv11Message) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
var cqAtRegex = regexp.MustCompile(`\[CQ:at,qq=(\d+)\]`)
|
||||
var cqImageRegex = regexp.MustCompile(`\[CQ:image,[^\]]*\]`)
|
||||
var cqURLRegex = regexp.MustCompile(`\burl=([^,\]]+)`)
|
||||
var boldRegex = regexp.MustCompile(`\*\*(.+?)\*\*`)
|
||||
|
||||
Reference in New Issue
Block a user