fix: 平台消息身份传递 — AI-Core 收到正确昵称而非永远 fallback 到管理员
- forwardToAICore 新增 nickname 字段,格式 "昵称 (QQ号)" 明确标识发送者 - 解决非管理员用户 @昔涟 时 AI 仍认为是管理员的身份污染问题 - 同时包含:管理员群聊插入抑制、markdown 粗体剥离、SearXNG 容器、ethend 窗口隐藏 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -84,7 +84,7 @@ func main() {
|
|||||||
|
|
||||||
// Routing decisions.
|
// Routing decisions.
|
||||||
isAdmin := mapper.IsAdmin(msg.Platform, msg.OriginalSenderUID)
|
isAdmin := mapper.IsAdmin(msg.Platform, msg.OriginalSenderUID)
|
||||||
isMentioned, mentionReason := detectAdminMention(msg, mapper, cfg)
|
isMentioned, _ := detectAdminMention(msg, mapper, cfg)
|
||||||
isBotMentioned := msg.BotUID != "" && containsString(msg.Mentions, msg.BotUID)
|
isBotMentioned := msg.BotUID != "" && containsString(msg.Mentions, msg.BotUID)
|
||||||
isSilent := cfg.PlatformSilentEnabled && !isAdmin && !isBotMentioned
|
isSilent := cfg.PlatformSilentEnabled && !isAdmin && !isBotMentioned
|
||||||
|
|
||||||
@@ -113,7 +113,6 @@ func main() {
|
|||||||
|
|
||||||
// Extract image URLs for vision/OCR processing (admin + bot-mentioned + admin-mentioned only).
|
// Extract image URLs for vision/OCR processing (admin + bot-mentioned + admin-mentioned only).
|
||||||
imageURLs := getImageURLs(msg)
|
imageURLs := getImageURLs(msg)
|
||||||
includeImages := isAdmin || isBotMentioned || isMentioned
|
|
||||||
|
|
||||||
// For group chats, use a channel-based user ID to share context between admin and regular users.
|
// For group chats, use a channel-based user ID to share context between admin and regular users.
|
||||||
chatUserID := msg.SenderID
|
chatUserID := msg.SenderID
|
||||||
@@ -123,6 +122,11 @@ func main() {
|
|||||||
groupSessionID := fmt.Sprintf("platform_%s_%s", msg.Platform, msg.ChannelID)
|
groupSessionID := fmt.Sprintf("platform_%s_%s", msg.Platform, msg.ChannelID)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
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)
|
||||||
|
|
||||||
case isAdmin:
|
case isAdmin:
|
||||||
msg.RouteType = "normal"
|
msg.RouteType = "normal"
|
||||||
response, routeErr = forwardToAICore(cfg, msg, "text", chatUserID, groupSessionID, imageURLs)
|
response, routeErr = forwardToAICore(cfg, msg, "text", chatUserID, groupSessionID, imageURLs)
|
||||||
@@ -132,19 +136,11 @@ func main() {
|
|||||||
response, routeErr = forwardToAICore(cfg, msg, "text", chatUserID, groupSessionID, imageURLs)
|
response, routeErr = forwardToAICore(cfg, msg, "text", chatUserID, groupSessionID, imageURLs)
|
||||||
|
|
||||||
case isMentioned:
|
case isMentioned:
|
||||||
msg.RouteType = "admin_mention"
|
// Non-admin user mentioned an admin. Don't respond in channel —
|
||||||
enhancedContent := fmt.Sprintf(
|
// the admin already gets QQ's native @notification. Observe silently.
|
||||||
"[来自%s平台 %s 频道 %s 的用户 %s 说]\n%s\n\n[系统提示:此消息提及了管理员(原因:%s)。请参考以上消息内容判断是否需要关注。]",
|
msg.RouteType = "silent"
|
||||||
msg.Platform, msg.ChannelType, msg.ChannelID, msg.SenderName, msg.Content, mentionReason,
|
namespace := buildMemoryNamespace(msg.Platform, msg.ChannelType, msg.ChannelID)
|
||||||
)
|
response, routeErr = forwardToAICore(cfg, msg, "platform_silent", namespace, namespace, nil)
|
||||||
originalContent := msg.Content
|
|
||||||
msg.Content = enhancedContent
|
|
||||||
if includeImages {
|
|
||||||
response, routeErr = forwardToAICore(cfg, msg, "text", "admin", "admin-session-main", imageURLs)
|
|
||||||
} else {
|
|
||||||
response, routeErr = forwardToAICore(cfg, msg, "text", "admin", "admin-session-main", nil)
|
|
||||||
}
|
|
||||||
msg.Content = originalContent
|
|
||||||
|
|
||||||
case isSilent:
|
case isSilent:
|
||||||
msg.RouteType = "silent"
|
msg.RouteType = "silent"
|
||||||
@@ -191,7 +187,7 @@ func main() {
|
|||||||
Direction: "outgoing",
|
Direction: "outgoing",
|
||||||
Platform: msg.Platform,
|
Platform: msg.Platform,
|
||||||
ChannelID: msg.ChannelID,
|
ChannelID: msg.ChannelID,
|
||||||
SenderID: msg.OriginalSenderUID,
|
SenderID: msg.BotUID,
|
||||||
SenderName: "Cyrene",
|
SenderName: "Cyrene",
|
||||||
Content: rm.Content,
|
Content: rm.Content,
|
||||||
ContentType: "text",
|
ContentType: "text",
|
||||||
@@ -493,6 +489,31 @@ func containsString(list []string, val string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shouldAdminBeSilent checks if admin is talking to other users in a group.
|
||||||
|
// Returns true if 昔涟 should not interrupt (route as silent observation instead).
|
||||||
|
func shouldAdminBeSilent(msg *bridge.UnifiedMessage, router *bridge.PlatformRouter) bool {
|
||||||
|
if msg.ChannelType != "group" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Rule 1: Admin @mentions someone other than the bot → talking to them, don't interrupt.
|
||||||
|
for _, m := range msg.Mentions {
|
||||||
|
if m != msg.BotUID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Rule 2: Recent context shows a conversation with non-admin users.
|
||||||
|
// Note: updateContext runs before this handler, so RecentSenders already
|
||||||
|
// includes the current message. Check the second-to-last sender instead.
|
||||||
|
ctx := router.GetContext(msg.Platform, msg.ChannelID)
|
||||||
|
if ctx != nil && len(ctx.RecentSenders) >= 2 {
|
||||||
|
prevSender := ctx.RecentSenders[len(ctx.RecentSenders)-2]
|
||||||
|
if prevSender != msg.OriginalSenderUID && prevSender != msg.BotUID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// getImageURLs extracts image attachment URLs from a UnifiedMessage.
|
// getImageURLs extracts image attachment URLs from a UnifiedMessage.
|
||||||
func getImageURLs(msg *bridge.UnifiedMessage) []string {
|
func getImageURLs(msg *bridge.UnifiedMessage) []string {
|
||||||
if len(msg.Attachments) == 0 {
|
if len(msg.Attachments) == 0 {
|
||||||
@@ -516,6 +537,7 @@ func forwardToAICore(cfg *config.Config, msg *bridge.UnifiedMessage, mode, userI
|
|||||||
"message": msg.Content,
|
"message": msg.Content,
|
||||||
"mode": mode,
|
"mode": mode,
|
||||||
"routing": msg.RouteType,
|
"routing": msg.RouteType,
|
||||||
|
"nickname": fmt.Sprintf("%s (%s)", msg.SenderName, msg.OriginalSenderUID),
|
||||||
"source": map[string]string{
|
"source": map[string]string{
|
||||||
"platform": msg.Platform,
|
"platform": msg.Platform,
|
||||||
"channel_id": msg.ChannelID,
|
"channel_id": msg.ChannelID,
|
||||||
|
|||||||
@@ -280,10 +280,7 @@ func (a *Adapter) ToUnified(rawMessage interface{}) (*bridge.UnifiedMessage, err
|
|||||||
func (a *Adapter) FromUnified(response *bridge.UnifiedResponse) ([]bridge.PlatformMessage, error) {
|
func (a *Adapter) FromUnified(response *bridge.UnifiedResponse) ([]bridge.PlatformMessage, error) {
|
||||||
var msgs []bridge.PlatformMessage
|
var msgs []bridge.PlatformMessage
|
||||||
for _, rm := range response.Messages {
|
for _, rm := range response.Messages {
|
||||||
content := rm.Content
|
content := convertMarkdownToQQ(rm.Content)
|
||||||
if rm.FormatMode == "markdown" {
|
|
||||||
content = convertMarkdownToQQ(rm.Content)
|
|
||||||
}
|
|
||||||
runes := []rune(content)
|
runes := []rune(content)
|
||||||
if len(runes) > 200 {
|
if len(runes) > 200 {
|
||||||
content = string(runes[:200])
|
content = string(runes[:200])
|
||||||
@@ -421,6 +418,9 @@ func extractText(msg *OBv11Message) string {
|
|||||||
|
|
||||||
var cqImageRegex = regexp.MustCompile(`\[CQ:image,[^\]]*\]`)
|
var cqImageRegex = regexp.MustCompile(`\[CQ:image,[^\]]*\]`)
|
||||||
var cqURLRegex = regexp.MustCompile(`\burl=([^,\]]+)`)
|
var cqURLRegex = regexp.MustCompile(`\burl=([^,\]]+)`)
|
||||||
|
var boldRegex = regexp.MustCompile(`\*\*(.+?)\*\*`)
|
||||||
|
var italicRegex = regexp.MustCompile(`\*(.+?)\*`)
|
||||||
|
var strikethroughRegex = regexp.MustCompile(`~~(.+?)~~`)
|
||||||
|
|
||||||
// extractAttachments extracts image URLs from OBv11Message.
|
// extractAttachments extracts image URLs from OBv11Message.
|
||||||
// Handles both string format (CQ codes in raw_message) and array format (parsed segments).
|
// Handles both string format (CQ codes in raw_message) and array format (parsed segments).
|
||||||
@@ -473,8 +473,11 @@ func extractAttachments(msg *OBv11Message) []bridge.Attachment {
|
|||||||
return attachments
|
return attachments
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertMarkdownToQQ converts common markdown to QQ-supported format.
|
// convertMarkdownToQQ converts markdown to QQ plain-text format.
|
||||||
func convertMarkdownToQQ(md string) string {
|
func convertMarkdownToQQ(md string) string {
|
||||||
|
md = boldRegex.ReplaceAllString(md, "$1")
|
||||||
|
md = italicRegex.ReplaceAllString(md, "$1")
|
||||||
|
md = strikethroughRegex.ReplaceAllString(md, "$1")
|
||||||
md = removeHeadings(md)
|
md = removeHeadings(md)
|
||||||
md = removeCodeBlocks(md)
|
md = removeCodeBlocks(md)
|
||||||
return md
|
return md
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ type PlatformRouter struct {
|
|||||||
|
|
||||||
// ChannelContext stores the active conversation state for a channel.
|
// ChannelContext stores the active conversation state for a channel.
|
||||||
type ChannelContext struct {
|
type ChannelContext struct {
|
||||||
Platform string
|
Platform string
|
||||||
ChannelID string
|
ChannelID string
|
||||||
ChannelType string
|
ChannelType string
|
||||||
LastUserMsg string
|
LastUserMsg string
|
||||||
MessageCount int
|
LastSenderUID string
|
||||||
|
RecentSenders []string // last 5 sender UIDs (original platform UIDs)
|
||||||
|
MessageCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlatformRouter(mapper *IdentityMapper, checker *permissions.Checker) *PlatformRouter {
|
func NewPlatformRouter(mapper *IdentityMapper, checker *permissions.Checker) *PlatformRouter {
|
||||||
@@ -207,6 +209,11 @@ func (r *PlatformRouter) updateContext(msg *UnifiedMessage) {
|
|||||||
r.contexts[key] = ctx
|
r.contexts[key] = ctx
|
||||||
}
|
}
|
||||||
ctx.LastUserMsg = msg.Content
|
ctx.LastUserMsg = msg.Content
|
||||||
|
ctx.LastSenderUID = msg.OriginalSenderUID
|
||||||
|
ctx.RecentSenders = append(ctx.RecentSenders, msg.OriginalSenderUID)
|
||||||
|
if len(ctx.RecentSenders) > 5 {
|
||||||
|
ctx.RecentSenders = ctx.RecentSenders[len(ctx.RecentSenders)-5:]
|
||||||
|
}
|
||||||
ctx.MessageCount++
|
ctx.MessageCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,18 @@ services:
|
|||||||
- "4222:4222"
|
- "4222:4222"
|
||||||
- "8222:8222"
|
- "8222:8222"
|
||||||
|
|
||||||
|
# ========== SearXNG 搜索引擎 ==========
|
||||||
|
searxng:
|
||||||
|
image: searxng/searxng:latest
|
||||||
|
container_name: cyrene_searxng
|
||||||
|
volumes:
|
||||||
|
- ./searxng/settings.yml:/etc/searxng/settings.yml:ro
|
||||||
|
environment:
|
||||||
|
SEARXNG_SETTINGS_PATH: /etc/searxng/settings.yml
|
||||||
|
ports:
|
||||||
|
- "8088:8080"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pg_data:
|
pg_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
|||||||
@@ -86,10 +86,6 @@ function detectDockerServices() {
|
|||||||
for (const m of ports.matchAll(/:(\d+)->/g)) {
|
for (const m of ports.matchAll(/:(\d+)->/g)) {
|
||||||
detectedPorts.add(parseInt(m[1]));
|
detectedPorts.add(parseInt(m[1]));
|
||||||
}
|
}
|
||||||
for (const m of ports.matchAll(/(\d+)\/(tcp|udp)/g)) {
|
|
||||||
detectedPorts.add(parseInt(m[1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [id, svc] of Object.entries(SERVICES)) {
|
for (const [id, svc] of Object.entries(SERVICES)) {
|
||||||
if (svc.port && detectedPorts.has(svc.port)) {
|
if (svc.port && detectedPorts.has(svc.port)) {
|
||||||
result.set(id, {
|
result.set(id, {
|
||||||
@@ -306,6 +302,7 @@ class ProcessManager extends EventEmitter {
|
|||||||
env,
|
env,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
shell: needsShell,
|
shell: needsShell,
|
||||||
|
windowsHide: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stdout.on('data', (data) => {
|
child.stdout.on('data', (data) => {
|
||||||
@@ -434,6 +431,7 @@ class ProcessManager extends EventEmitter {
|
|||||||
cwd: svc.cwd,
|
cwd: svc.cwd,
|
||||||
env: { ...process.env, GOPROXY: 'https://goproxy.cn,direct', GOWORK: 'off' },
|
env: { ...process.env, GOPROXY: 'https://goproxy.cn,direct', GOWORK: 'off' },
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
windowsHide: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
|
|||||||
Reference in New Issue
Block a user