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>
This commit is contained in:
2026-05-31 11:49:36 +08:00
parent 677385ec17
commit 3ad728406e
13 changed files with 587 additions and 156 deletions
+52 -8
View File
@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
@@ -46,8 +47,13 @@ import (
var cfg Config
func main() {
// 自动加载 .env 文件(来自仓库根目录)
if err := godotenv.Load("../../.env"); err != nil {
// 自动加载 .env 文件(优先从可执行文件位置反推仓库根目录)
_ = godotenv.Load() // 先尝试当前目录
if exe, err := os.Executable(); err == nil {
_ = godotenv.Load(filepath.Join(filepath.Dir(exe), "..", "..", ".env"))
}
// 兜底:如果 LLM_MODEL 仍未设置,打印提示
if os.Getenv("LLM_MODEL") == "" {
log.Println("ℹ 未找到 .env 文件,将使用环境变量或默认值")
}
@@ -431,6 +437,36 @@ func main() {
})
})
// LLM 调用 SSE 实时推送
mux.HandleFunc("/api/v1/llm-calls/stream", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming not supported", http.StatusInternalServerError)
return
}
ch, done := llm.SubscribeCalls()
defer llm.UnsubscribeCalls(ch)
for {
select {
case rec := <-ch:
data, _ := json.Marshal(rec)
fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
case <-done:
return
case <-r.Context().Done():
return
}
}
})
// 工具调用记录
mux.HandleFunc("/api/v1/tools/calls", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
@@ -724,6 +760,13 @@ func handleChat(
Images []string `json:"images,omitempty"` // 图片 base64 data URL
Mode string `json:"mode"`
Nickname string `json:"nickname,omitempty"`
Source struct {
Platform string `json:"platform"`
ChannelID string `json:"channel_id"`
ChannelType string `json:"channel_type"`
SenderName string `json:"sender_name"`
OriginalUID string `json:"original_uid"`
} `json:"source,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求体", http.StatusBadRequest)
@@ -781,12 +824,13 @@ func handleChat(
// 2. 调用 Orchestrator 处理(替代原有的线性处理流程)
// Orchestrator 内部处理:意图分析 → 子会话分派 → 结果汇总 → 综合生成回复
eventCh, err := orch.ProcessInput(ctx, orchestrator.ProcessParams{
UserID: req.UserID,
SessionID: req.SessionID,
Message: req.Message,
Images: req.Images,
Mode: req.Mode,
Nickname: userNickname,
UserID: req.UserID,
SessionID: req.SessionID,
Message: req.Message,
Images: req.Images,
Mode: req.Mode,
Nickname: userNickname,
ChannelType: req.Source.ChannelType,
})
if err != nil {
errData, _ := json.Marshal(map[string]string{"delta": "", "error": fmt.Sprintf("处理失败: %v", err)})