fix: 死锁修复、管理员权限集中管控、群聊频率控制、表情映射修复
thinker.go: 移除所有 defer t.muUnlock() 持锁跨阻塞调用的模式,消除8处死锁点 - performThink: defer→立即解锁,LLM调用不再持锁 - lightThinkLoop: defer在for循环内→第2次迭代自死锁 - resetSilenceTimer: defer持锁调performThink - UpdatePresence: defer持锁调time.Sleep+performThink - storeThought: defer+panic→锁泄露; 移除extractProactiveMessage嵌套锁 is_admin三层防御: - synthesizer: 系统提示词注入管理员/非管理员身份标签 - iot_provider: 非管理员直接拒绝IoT操作 - plugin-manager: ToolDefinition.AdminOnly自动拦截,集中管控 群聊优化: - group_ambient: 强化审查指令,【不发送】自审查标签 - 群聊间隔4s→3s,最多2条/轮 - 工具失败也推跟进消息,避免沉默 平台桥接: - 日志文件名使用适配器唯一标识符(ConfigName) - QQ表情映射替换为官方116条目数据 - CQ表情保留名称/ID
This commit is contained in:
@@ -80,7 +80,7 @@ func main() {
|
||||
msgLogger.Log(logging.LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Direction: "incoming",
|
||||
Platform: msg.Platform,
|
||||
Platform: logKey(msg),
|
||||
ChannelID: msg.ChannelID,
|
||||
SenderID: msg.OriginalSenderUID,
|
||||
SenderName: msg.OriginalSenderName,
|
||||
@@ -100,7 +100,7 @@ func main() {
|
||||
msgLogger.Log(logging.LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Direction: "incoming",
|
||||
Platform: msg.Platform,
|
||||
Platform: logKey(msg),
|
||||
ChannelID: msg.ChannelID,
|
||||
SenderID: msg.OriginalSenderUID,
|
||||
SenderName: msg.OriginalSenderName,
|
||||
@@ -186,7 +186,7 @@ func main() {
|
||||
msgLogger.Log(logging.LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Direction: "error",
|
||||
Platform: msg.Platform,
|
||||
Platform: logKey(msg),
|
||||
ChannelID: msg.ChannelID,
|
||||
SenderID: msg.OriginalSenderUID,
|
||||
Success: false,
|
||||
@@ -241,7 +241,7 @@ func main() {
|
||||
msgLogger.Log(logging.LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Direction: "error",
|
||||
Platform: msg.Platform,
|
||||
Platform: logKey(msg),
|
||||
ChannelID: msg.ChannelID,
|
||||
SenderID: msg.OriginalSenderUID,
|
||||
Success: false,
|
||||
@@ -256,7 +256,7 @@ func main() {
|
||||
msgLogger.Log(logging.LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Direction: "outgoing",
|
||||
Platform: msg.Platform,
|
||||
Platform: logKey(msg),
|
||||
ChannelID: msg.ChannelID,
|
||||
SenderID: msg.BotUID,
|
||||
SenderName: "Cyrene",
|
||||
@@ -438,10 +438,15 @@ func startOBv11Readers(router *bridge.PlatformRouter) {
|
||||
messageType := msg.MessageType
|
||||
userID := msg.UserID
|
||||
groupID := msg.GroupID
|
||||
// Filter non-empty messages.
|
||||
// Filter non-empty messages and strip 【不发送】 self-censored ones.
|
||||
var toSend []bridge.ResponseMessage
|
||||
for _, rm := range response.Messages {
|
||||
if rm.Content != "" {
|
||||
// 昔涟可以加 【不发送】 标签表示这条消息不发送——她醒了但选择不说话。
|
||||
if strings.Contains(rm.Content, "【不发送】") {
|
||||
fmt.Printf("[qq:%s] 昔涟自我审查,跳过发送: %s\n", adapterKey, truncateString(rm.Content, 60))
|
||||
continue
|
||||
}
|
||||
rm.Content = qqadapter.ConvertMarkdownToQQ(rm.Content)
|
||||
toSend = append(toSend, rm)
|
||||
}
|
||||
@@ -458,6 +463,16 @@ func startOBv11Readers(router *bridge.PlatformRouter) {
|
||||
if interval <= 0 {
|
||||
interval = 2 * time.Second
|
||||
}
|
||||
// 群聊用更长间隔避免刷屏,且限制最多2条消息
|
||||
if messageType == "group" {
|
||||
minGroupInterval := 3 * time.Second
|
||||
if interval < minGroupInterval {
|
||||
interval = minGroupInterval
|
||||
}
|
||||
if len(toSend) > 2 {
|
||||
toSend = toSend[:2]
|
||||
}
|
||||
}
|
||||
for i, rm := range toSend {
|
||||
if i > 0 && interval > 0 {
|
||||
select {
|
||||
@@ -1095,6 +1110,16 @@ func formatDuration(d time.Duration) string {
|
||||
return fmt.Sprintf("%dh", h)
|
||||
}
|
||||
|
||||
// logKey returns the log file key for a message.
|
||||
// Uses the adapter config name (e.g., "obv11-main") if set, falling back to platform type (e.g., "obv11").
|
||||
// This ensures each config writes to its own uniquely-named log file.
|
||||
func logKey(msg *bridge.UnifiedMessage) string {
|
||||
if msg.AdapterName != "" {
|
||||
return msg.AdapterName
|
||||
}
|
||||
return msg.Platform
|
||||
}
|
||||
|
||||
// truncateString truncates a string to maxRunes runes, appending "…" if truncated.
|
||||
func truncateString(s string, maxRunes int) string {
|
||||
runes := []rune(s)
|
||||
|
||||
@@ -805,6 +805,141 @@ var cqSimplifyMap = map[string]string{
|
||||
"video": "[视频]",
|
||||
"file": "[文件]",
|
||||
}
|
||||
// qqFaceName maps QQ face/emoji IDs to their Chinese names.
|
||||
// Source: Official QQ Bot API Emoji List (https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html)
|
||||
var qqFaceName = map[string]string{
|
||||
"4": "得意",
|
||||
"5": "流泪",
|
||||
"8": "睡",
|
||||
"9": "大哭",
|
||||
"10": "尴尬",
|
||||
"12": "调皮",
|
||||
"14": "微笑",
|
||||
"16": "酷",
|
||||
"21": "可爱",
|
||||
"23": "傲慢",
|
||||
"24": "饥饿",
|
||||
"25": "困",
|
||||
"26": "惊恐",
|
||||
"27": "流汗",
|
||||
"28": "憨笑",
|
||||
"29": "悠闲",
|
||||
"30": "奋斗",
|
||||
"32": "疑问",
|
||||
"33": "嘘",
|
||||
"34": "晕",
|
||||
"38": "敲打",
|
||||
"39": "再见",
|
||||
"41": "发抖",
|
||||
"42": "爱情",
|
||||
"43": "跳跳",
|
||||
"49": "拥抱",
|
||||
"53": "蛋糕",
|
||||
"60": "咖啡",
|
||||
"63": "玫瑰",
|
||||
"66": "爱心",
|
||||
"74": "太阳",
|
||||
"75": "月亮",
|
||||
"76": "赞",
|
||||
"78": "握手",
|
||||
"79": "胜利",
|
||||
"85": "飞吻",
|
||||
"89": "西瓜",
|
||||
"96": "冷汗",
|
||||
"97": "擦汗",
|
||||
"98": "抠鼻",
|
||||
"99": "鼓掌",
|
||||
"100": "糗大了",
|
||||
"101": "坏笑",
|
||||
"102": "左哼哼",
|
||||
"103": "右哼哼",
|
||||
"104": "哈欠",
|
||||
"106": "委屈",
|
||||
"109": "左亲亲",
|
||||
"111": "可怜",
|
||||
"116": "示爱",
|
||||
"118": "抱拳",
|
||||
"120": "拳头",
|
||||
"122": "爱你",
|
||||
"123": "NO",
|
||||
"124": "OK",
|
||||
"125": "转圈",
|
||||
"129": "挥手",
|
||||
"144": "喝彩",
|
||||
"147": "棒棒糖",
|
||||
"171": "茶",
|
||||
"173": "泪奔",
|
||||
"174": "无奈",
|
||||
"175": "卖萌",
|
||||
"176": "小纠结",
|
||||
"179": "doge",
|
||||
"180": "惊喜",
|
||||
"181": "骚扰",
|
||||
"182": "笑哭",
|
||||
"183": "我最美",
|
||||
"201": "点赞",
|
||||
"203": "托脸",
|
||||
"212": "托腮",
|
||||
"214": "啵啵",
|
||||
"219": "蹭一蹭",
|
||||
"222": "抱抱",
|
||||
"227": "拍手",
|
||||
"232": "佛系",
|
||||
"240": "喷脸",
|
||||
"243": "甩头",
|
||||
"246": "加油抱抱",
|
||||
"262": "脑阔疼",
|
||||
"264": "捂脸",
|
||||
"265": "辣眼睛",
|
||||
"266": "哦哟",
|
||||
"267": "头秃",
|
||||
"268": "问号脸",
|
||||
"269": "暗中观察",
|
||||
"270": "emm",
|
||||
"271": "吃瓜",
|
||||
"272": "呵呵哒",
|
||||
"273": "我酸了",
|
||||
"277": "汪汪",
|
||||
"278": "汗",
|
||||
"281": "无眼笑",
|
||||
"282": "敬礼",
|
||||
"284": "面无表情",
|
||||
"285": "摸鱼",
|
||||
"287": "哦",
|
||||
"289": "睁眼",
|
||||
"290": "敲开心",
|
||||
"293": "摸锦鲤",
|
||||
"294": "期待",
|
||||
"297": "拜谢",
|
||||
"298": "元宝",
|
||||
"299": "牛啊",
|
||||
"305": "右亲亲",
|
||||
"306": "牛气冲天",
|
||||
"307": "喵喵",
|
||||
"314": "仔细分析",
|
||||
"315": "加油",
|
||||
"318": "崇拜",
|
||||
"319": "比心",
|
||||
"320": "庆祝",
|
||||
"322": "拒绝",
|
||||
"324": "吃糖",
|
||||
"326": "生气",
|
||||
}
|
||||
|
||||
// resolveFaceName returns the display name for a face CQ code.
|
||||
// Uses name field if present (NapCat array format), otherwise looks up the face ID in qqFaceName map.
|
||||
func resolveFaceName(match string) string {
|
||||
if name := extractCQParam(match, "name"); name != "" {
|
||||
return name
|
||||
}
|
||||
if id := extractCQParam(match, "id"); id != "" {
|
||||
if name, ok := qqFaceName[id]; ok {
|
||||
return name
|
||||
}
|
||||
return id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// simplifyCQCodes replaces [CQ:type,...] codes with human-readable labels.
|
||||
func simplifyCQCodes(s string) string {
|
||||
@@ -826,6 +961,13 @@ func simplifyCQCodes(s string) string {
|
||||
}
|
||||
return "[卡片消息]"
|
||||
}
|
||||
if typ == "face" {
|
||||
faceName := resolveFaceName(match)
|
||||
if faceName != "" {
|
||||
return "[表情 " + faceName + "]"
|
||||
}
|
||||
return "[表情]"
|
||||
}
|
||||
if label, ok := cqSimplifyMap[typ]; ok {
|
||||
return label
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ func (h *BridgeHandler) sendProactive(w http.ResponseWriter, r *http.Request) {
|
||||
if msgType == "private" {
|
||||
chID = req.UserID
|
||||
}
|
||||
h.logFn(req.Platform, chID, "Cyrene", fullMsg, true)
|
||||
h.logFn(adapterName, chID, "Cyrene", fullMsg, true)
|
||||
}
|
||||
}
|
||||
if sendErr != nil {
|
||||
|
||||
@@ -29,13 +29,9 @@ func (h *LogHandler) handleLogs(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve platform type from config name (e.g. "obv11-home" → "obv11").
|
||||
// Use the name directly as the log key. Each config has its own log file
|
||||
// named by its unique identifier (e.g., "obv11-main.log").
|
||||
platform := name
|
||||
if h.store != nil {
|
||||
if cfg, err := h.store.Get(name); err == nil && cfg.Platform != "" {
|
||||
platform = cfg.Platform
|
||||
}
|
||||
}
|
||||
|
||||
limit := 100
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
|
||||
Reference in New Issue
Block a user