Files
Cyrene/backend/ai-core/internal/llm/call_log.go
T
AskaEth 3ad728406e 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>
2026-05-31 11:49:36 +08:00

123 lines
2.7 KiB
Go

package llm
import (
"sync"
"time"
)
// CallRecord records a single LLM API call.
type CallRecord struct {
Time time.Time `json:"time"`
Model string `json:"model"`
Duration time.Duration `json:"duration_ms"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// CallLogger is a thread-safe ring buffer for LLM call records.
type CallLogger struct {
mu sync.RWMutex
records []CallRecord
capacity int
head int
size int
}
var globalCallLogger = &CallLogger{capacity: 500}
// LogCall records an LLM call. Safe for concurrent use.
func LogCall(r CallRecord) {
globalCallLogger.log(r)
}
// GetCalls returns recent call records, newest first.
func GetCalls(limit int) []CallRecord {
return globalCallLogger.get(limit)
}
func (cl *CallLogger) log(r CallRecord) {
cl.mu.Lock()
defer cl.mu.Unlock()
if cl.records == nil {
cl.records = make([]CallRecord, cl.capacity)
}
r.Time = time.Now()
cl.records[cl.head] = r
cl.head = (cl.head + 1) % cl.capacity
if cl.size < cl.capacity {
cl.size++
}
broadcastCall(r)
}
func (cl *CallLogger) get(limit int) []CallRecord {
cl.mu.RLock()
defer cl.mu.RUnlock()
if limit <= 0 || limit > cl.size {
limit = cl.size
}
result := make([]CallRecord, limit)
for i := 0; i < limit; i++ {
idx := (cl.head - 1 - i) % cl.capacity
if idx < 0 {
idx += cl.capacity
}
result[i] = cl.records[idx]
}
return result
}
// --- SSE subscriber system ---
type callSubscriber struct {
ch chan CallRecord
done chan struct{}
}
var (
callSubscribers []*callSubscriber
callSubscribersMu sync.RWMutex
)
// SubscribeCalls returns a channel that receives new CallRecords and a done channel.
func SubscribeCalls() (<-chan CallRecord, <-chan struct{}) {
ch := make(chan CallRecord, 20)
done := make(chan struct{})
callSubscribersMu.Lock()
callSubscribers = append(callSubscribers, &callSubscriber{ch: ch, done: done})
callSubscribersMu.Unlock()
return ch, done
}
// UnsubscribeCalls removes a subscriber. Safe to call multiple times.
func UnsubscribeCalls(ch <-chan CallRecord) {
callSubscribersMu.Lock()
defer callSubscribersMu.Unlock()
for i, s := range callSubscribers {
if s.ch == ch {
close(s.done)
callSubscribers = append(callSubscribers[:i], callSubscribers[i+1:]...)
return
}
}
}
func broadcastCall(r CallRecord) {
callSubscribersMu.RLock()
defer callSubscribersMu.RUnlock()
for _, s := range callSubscribers {
select {
case s.ch <- r:
default:
}
}
}