Files
Cyrene/backend/platform-bridge/internal/logging/logger.go
T
AskaEth 47dce276a4 fix: platform_silent记忆提取 + 群聊上下文整合 + 多QQ实例支持
- platform_silent模式接入Orchestrator记忆提取:被动观察群聊时提取值得记住的信息到对应命名空间
- post_chat后台思考注入平台观察:对话后思考也能看到群聊摘要
- QQ适配器:OneBot v11 self_id动态捕获、CQ图片URL提取、视觉+OCR并行处理
- Router解耦:ConfigName/PlatformName分离,支持多QQ实例独立连接
- 黑白名单功能:后端API + Ethend代理 + UI面板
- \n\n双换行断句:AI回复按双换行分割为多条消息按间隔发送
- @提及修复:bot自感知UID进行@检测
- 群聊上下文共享:channel-based userID避免记忆碎片化
- 消息日志显示处理后内容而非原始SSE数据
- platform-bridge Dockerfile + docker-compose.yml更新

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 09:37:18 +08:00

173 lines
4.0 KiB
Go

package logging
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"time"
)
// LogEntry represents one message log record.
type LogEntry struct {
Timestamp time.Time `json:"timestamp"`
Direction string `json:"direction"` // "incoming" or "outgoing"
Platform string `json:"platform"`
ChannelID string `json:"channel_id"`
SenderID string `json:"sender_id"`
SenderName string `json:"sender_name"`
Content string `json:"content"`
ContentType string `json:"content_type"`
MessageID string `json:"message_id,omitempty"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// LogListener receives log entries as they are written.
type LogListener func(LogEntry)
// Logger writes message logs to per-platform JSONL files.
type Logger struct {
mu sync.Mutex
dir string
files map[string]*os.File
listeners []LogListener
}
// OnLog registers a listener that is called for every log entry written.
// The listener is called synchronously; avoid heavy work in the callback.
func (l *Logger) OnLog(fn LogListener) {
l.mu.Lock()
defer l.mu.Unlock()
l.listeners = append(l.listeners, fn)
}
// NewLogger creates a Logger, ensuring the log directory exists.
func NewLogger(dir string) (*Logger, error) {
if err := os.MkdirAll(dir, 0750); err != nil {
return nil, fmt.Errorf("create log dir: %w", err)
}
return &Logger{
dir: dir,
files: make(map[string]*os.File),
}, nil
}
// Log writes a log entry to the appropriate platform log file.
func (l *Logger) Log(entry LogEntry) error {
if entry.Timestamp.IsZero() {
entry.Timestamp = time.Now()
}
f, err := l.getOrCreateFile(entry.Platform)
if err != nil {
return err
}
data, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("marshal log entry: %w", err)
}
l.mu.Lock()
if _, err := f.Write(append(data, '\n')); err != nil {
l.mu.Unlock()
return fmt.Errorf("write log: %w", err)
}
if err := f.Sync(); err != nil {
l.mu.Unlock()
return err
}
listeners := make([]LogListener, len(l.listeners))
copy(listeners, l.listeners)
l.mu.Unlock()
// Notify listeners outside the lock.
for _, fn := range listeners {
fn(entry)
}
return nil
}
// ReadLogs reads the last N log entries for a platform, newest first.
func (l *Logger) ReadLogs(platform string, limit int) ([]LogEntry, error) {
if limit <= 0 || limit > 1000 {
limit = 1000
}
l.mu.Lock()
// Flush any pending writes to the file before reading.
if f, ok := l.files[platform]; ok {
f.Sync()
}
l.mu.Unlock()
path := filepath.Join(l.dir, platform+".log")
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return []LogEntry{}, nil
}
return nil, fmt.Errorf("open log file: %w", err)
}
defer f.Close()
// Read all lines, keep only the last `limit`.
var lines []string
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("read log file: %w", err)
}
// Take last N lines and reverse.
start := len(lines) - limit
if start < 0 {
start = 0
}
lines = lines[start:]
entries := make([]LogEntry, 0, len(lines))
for i := len(lines) - 1; i >= 0; i-- {
var entry LogEntry
if err := json.Unmarshal([]byte(lines[i]), &entry); err != nil {
continue // Skip corrupted lines.
}
entries = append(entries, entry)
}
return entries, nil
}
// Close closes all open log file handles.
func (l *Logger) Close() error {
l.mu.Lock()
defer l.mu.Unlock()
for _, f := range l.files {
f.Close()
}
l.files = make(map[string]*os.File)
return nil
}
func (l *Logger) getOrCreateFile(platform string) (*os.File, error) {
l.mu.Lock()
defer l.mu.Unlock()
if f, ok := l.files[platform]; ok {
return f, nil
}
path := filepath.Join(l.dir, platform+".log")
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640)
if err != nil {
return nil, fmt.Errorf("open log file %s: %w", path, err)
}
l.files[platform] = f
return f, nil
}