fix: TLS连接池复用 + 禁言检测 + 降级回复静默
- llm/openai.go: 移除DisableKeepAlives,启用连接池(100idle/20perhost/90s) 修复频繁TLS握手超时问题 - qq/adapter.go: 监听group_ban通知,发送前检查禁言状态 防止向被禁言群发消息导致风控 - cmd/main.go: 降级回复仅管理员私聊,群聊静默失败避免怪话 - protocol.go: OBv11Message新增Duration字段 - bridge/router.go: 处理noticeToUnified返回nil(内部消费的通知)
This commit is contained in:
+18
-10
@@ -1221,11 +1221,16 @@ func handleChat(
|
|||||||
IsAdmin: req.IsAdmin,
|
IsAdmin: req.IsAdmin,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fallback := getFallbackMessage()
|
// 降级回复:仅对管理员私聊发送,群聊/频道静默失败避免怪话
|
||||||
log.Printf("[chat] ProcessInput 失败,使用降级回复: %v", err)
|
if req.IsAdmin && req.Source.ChannelType == "direct" {
|
||||||
fallbackData, _ := json.Marshal(map[string]string{"delta": fallback, "status": "fallback"})
|
fallback := getFallbackMessage()
|
||||||
fmt.Fprintf(w, "data: %s\n\n", fallbackData)
|
log.Printf("[chat] ProcessInput 失败,发送降级回复: %v", err)
|
||||||
flusher.Flush()
|
fallbackData, _ := json.Marshal(map[string]string{"delta": fallback, "status": "fallback"})
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", fallbackData)
|
||||||
|
flusher.Flush()
|
||||||
|
} else {
|
||||||
|
log.Printf("[chat] ProcessInput 失败,群聊静默: %v", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1250,11 +1255,14 @@ func handleChat(
|
|||||||
|
|
||||||
case model.StreamError:
|
case model.StreamError:
|
||||||
log.Printf("[chat] 流式错误: %v", event.Error)
|
log.Printf("[chat] 流式错误: %v", event.Error)
|
||||||
fallback := getFallbackMessage()
|
// 降级回复:仅管理员私聊,群聊静默避免怪话
|
||||||
log.Printf("[chat] 流式事件错误,发送降级回复: %v", event.Error)
|
if req.IsAdmin && req.Source.ChannelType == "direct" {
|
||||||
fallbackData, _ := json.Marshal(map[string]string{"delta": fallback, "status": "fallback"})
|
fallback := getFallbackMessage()
|
||||||
fmt.Fprintf(w, "data: %s\n\n", fallbackData)
|
log.Printf("[chat] 流式错误,发送降级回复: %v", event.Error)
|
||||||
flusher.Flush()
|
fallbackData, _ := json.Marshal(map[string]string{"delta": fallback, "status": "fallback"})
|
||||||
|
fmt.Fprintf(w, "data: %s\n\n", fallbackData)
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
|
|
||||||
case model.StreamDelta:
|
case model.StreamDelta:
|
||||||
|
|||||||
@@ -41,9 +41,12 @@ func NewOpenAIProvider(cfg OpenAIConfig) *OpenAIProvider {
|
|||||||
cfg.Timeout = 60 * time.Second
|
cfg.Timeout = 60 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
// 克隆默认 Transport 并关闭 keep-alive,防止 context 取消后连接池脏连接导致全阻塞
|
// 使用连接池复用 TCP+TLS 连接,避免每次请求都重新 TLS 握手
|
||||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
tr.DisableKeepAlives = true
|
tr.MaxIdleConns = 100
|
||||||
|
tr.MaxIdleConnsPerHost = 20
|
||||||
|
tr.IdleConnTimeout = 90 * time.Second
|
||||||
|
tr.TLSHandshakeTimeout = 15 * time.Second
|
||||||
|
|
||||||
return &OpenAIProvider{
|
return &OpenAIProvider{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
|||||||
@@ -442,10 +442,23 @@ func startOBv11Readers(router *bridge.PlatformRouter) {
|
|||||||
fmt.Printf("[qq:%s] route error: %v\n", adapterKey, err)
|
fmt.Printf("[qq:%s] route error: %v\n", adapterKey, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// nil response = internally handled (e.g., mute tracking), skip sending.
|
||||||
|
if response == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if response != nil && len(response.Messages) > 0 && !hasOnlySilentMessages(response.Messages) {
|
if response != nil && len(response.Messages) > 0 && !hasOnlySilentMessages(response.Messages) {
|
||||||
messageType := msg.MessageType
|
messageType := msg.MessageType
|
||||||
userID := msg.UserID
|
userID := msg.UserID
|
||||||
groupID := msg.GroupID
|
groupID := msg.GroupID
|
||||||
|
// 检查群禁言状态,避免向被禁言的群发送消息导致风控
|
||||||
|
if messageType == "group" {
|
||||||
|
if cur, err := router.GetAdapter(adapterKey); err == nil {
|
||||||
|
if qa, ok := cur.(*qqadapter.Adapter); ok && qa.IsGroupMuted(groupID) {
|
||||||
|
fmt.Printf("[qq:%s] 群 %d 处于禁言状态,跳过发送\n", adapterKey, groupID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Filter non-empty messages and strip 【不发送】 self-censored ones.
|
// Filter non-empty messages and strip 【不发送】 self-censored ones.
|
||||||
var toSend []bridge.ResponseMessage
|
var toSend []bridge.ResponseMessage
|
||||||
for _, rm := range response.Messages {
|
for _, rm := range response.Messages {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -46,6 +47,9 @@ type Adapter struct {
|
|||||||
groupNames map[int64]string // group ID → group name cache
|
groupNames map[int64]string // group ID → group name cache
|
||||||
groupNamesMu sync.RWMutex
|
groupNamesMu sync.RWMutex
|
||||||
|
|
||||||
|
mutedGroups map[int64]time.Time // group ID → mute expiry (zero = indefinitely muted)
|
||||||
|
mutedGroupsMu sync.RWMutex
|
||||||
|
|
||||||
pendingResponses map[string]chan *OBv11APIResponse
|
pendingResponses map[string]chan *OBv11APIResponse
|
||||||
respMu sync.Mutex
|
respMu sync.Mutex
|
||||||
}
|
}
|
||||||
@@ -64,9 +68,42 @@ func NewAdapter(configID, configName, mode, port, accessToken, remoteURL string,
|
|||||||
sendIntervalMs: sendIntervalMs,
|
sendIntervalMs: sendIntervalMs,
|
||||||
pendingResponses: make(map[string]chan *OBv11APIResponse),
|
pendingResponses: make(map[string]chan *OBv11APIResponse),
|
||||||
groupNames: make(map[int64]string),
|
groupNames: make(map[int64]string),
|
||||||
|
mutedGroups: make(map[int64]time.Time),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsGroupMuted returns true if the bot is currently muted in the given group.
|
||||||
|
func (a *Adapter) IsGroupMuted(groupID int64) bool {
|
||||||
|
a.mutedGroupsMu.RLock()
|
||||||
|
expiry, ok := a.mutedGroups[groupID]
|
||||||
|
a.mutedGroupsMu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if expiry.IsZero() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return time.Now().Before(expiry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setGroupMuted marks the bot as muted in a group.
|
||||||
|
func (a *Adapter) setGroupMuted(groupID int64, duration int64) {
|
||||||
|
a.mutedGroupsMu.Lock()
|
||||||
|
defer a.mutedGroupsMu.Unlock()
|
||||||
|
if duration <= 0 {
|
||||||
|
a.mutedGroups[groupID] = time.Time{}
|
||||||
|
} else {
|
||||||
|
a.mutedGroups[groupID] = time.Now().Add(time.Duration(duration) * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearGroupMuted removes the mute status for a group.
|
||||||
|
func (a *Adapter) clearGroupMuted(groupID int64) {
|
||||||
|
a.mutedGroupsMu.Lock()
|
||||||
|
delete(a.mutedGroups, groupID)
|
||||||
|
a.mutedGroupsMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// SetHTTPConfig sets the optional HTTP API configuration.
|
// SetHTTPConfig sets the optional HTTP API configuration.
|
||||||
func (a *Adapter) SetHTTPConfig(url, token string) {
|
func (a *Adapter) SetHTTPConfig(url, token string) {
|
||||||
a.httpURL = url
|
a.httpURL = url
|
||||||
@@ -463,7 +500,23 @@ func (a *Adapter) ToUnified(rawMessage interface{}) (*bridge.UnifiedMessage, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// noticeToUnified converts an OBv11 notice event (poke/戳一戳 etc.) to a UnifiedMessage.
|
// noticeToUnified converts an OBv11 notice event (poke/戳一戳 etc.) to a UnifiedMessage.
|
||||||
|
// Returns nil when the notice is handled internally (e.g., mute tracking).
|
||||||
func (a *Adapter) noticeToUnified(msg *OBv11Message) (*bridge.UnifiedMessage, error) {
|
func (a *Adapter) noticeToUnified(msg *OBv11Message) (*bridge.UnifiedMessage, error) {
|
||||||
|
// Handle group mute/ban notices — track mute state internally.
|
||||||
|
if msg.NoticeType == "group_ban" && msg.GroupID != 0 {
|
||||||
|
botUID, _ := strconv.ParseInt(a.selfID, 10, 64)
|
||||||
|
if msg.UserID == botUID {
|
||||||
|
if msg.SubType == "ban" {
|
||||||
|
a.setGroupMuted(msg.GroupID, msg.Duration)
|
||||||
|
log.Printf("[qq] 群 %d 被禁言 (时长=%ds)", msg.GroupID, msg.Duration)
|
||||||
|
} else if msg.SubType == "lift_ban" {
|
||||||
|
a.clearGroupMuted(msg.GroupID)
|
||||||
|
log.Printf("[qq] 群 %d 解除禁言", msg.GroupID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil // internally handled, no chat message to dispatch
|
||||||
|
}
|
||||||
|
|
||||||
senderID := fmt.Sprintf("%d", msg.UserID)
|
senderID := fmt.Sprintf("%d", msg.UserID)
|
||||||
senderName := senderID
|
senderName := senderID
|
||||||
|
|
||||||
@@ -623,8 +676,8 @@ func (a *Adapter) ReadMessages(ctx context.Context, msgCh chan<- *OBv11Message)
|
|||||||
fmt.Printf("[qq:%s] self ID captured: %s\n", a.configName, a.selfID)
|
fmt.Printf("[qq:%s] self ID captured: %s\n", a.configName, a.selfID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch message and notice (poke, etc.) events.
|
// Dispatch message and notice events (poke, group_ban for mute tracking).
|
||||||
if msg.PostType == "message" || (msg.PostType == "notice" && msg.NoticeType == "notify" && msg.SubType == "poke") {
|
if msg.PostType == "message" || (msg.PostType == "notice" && (msg.NoticeType == "notify" || msg.NoticeType == "group_ban")) {
|
||||||
select {
|
select {
|
||||||
case msgCh <- &msg:
|
case msgCh <- &msg:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type OBv11Message struct {
|
|||||||
|
|
||||||
// Notice fields.
|
// Notice fields.
|
||||||
NoticeType string `json:"notice_type"`
|
NoticeType string `json:"notice_type"`
|
||||||
|
Duration int64 `json:"duration"` // group_ban: mute duration in seconds (0 = indefinite)
|
||||||
|
|
||||||
// Poke detail (sub_type === "poke").
|
// Poke detail (sub_type === "poke").
|
||||||
PokeDetail *OBv11PokeDetail `json:"poke_detail,omitempty"`
|
PokeDetail *OBv11PokeDetail `json:"poke_detail,omitempty"`
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ func (r *PlatformRouter) RouteMessage(adapterKey string, rawMsg interface{}) (*U
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("convert to unified: %w", err)
|
return nil, fmt.Errorf("convert to unified: %w", err)
|
||||||
}
|
}
|
||||||
|
// nil means the notice was handled internally (e.g., mute tracking) — no dispatch needed.
|
||||||
|
if unified == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Preserve original platform UID before identity mapping.
|
// Preserve original platform UID before identity mapping.
|
||||||
unified.OriginalSenderUID = unified.SenderID
|
unified.OriginalSenderUID = unified.SenderID
|
||||||
|
|||||||
Reference in New Issue
Block a user