feat: 全链路优化 — 死锁修复、MD3主题、上下文持久化、群聊自然化、打字状态、知识库
**死锁根因修复** - periodicThinkLoop:1015 orphaned lock → 删除(字段已原子化) - RecordUserMessage 隔离为 recordMu - atomic.Int64 替换 lastUserMessage/lastThinkTime 等 **MD3 / Android 17 主题** - 毛玻璃卡片 (backdrop-filter) - MD3 色彩令牌 (pink primary #f472b6) - icons.js 独立矢量图标库 + 运行时 emoji 替换 - 无边框卡片、圆角按钮、阴影层次 **上下文持久化** - AddMessage → saveToDB 异步写 PostgreSQL - LoadFromDB 恢复 (admin-session-main + 懒加载) - LLMMessage.Timestamp 字段 **群聊与适配器** - group_ambient 模式: 非@消息让 LLM 自己判断是否插话 - 戳一戳动作消息总是回复 - NapCat 打字状态 (set_input_status, 最小3秒显示) - HTTP API 配置 (http_url/http_token) **知识库 & 防编造** - knowledge.CanHandle 对 chat 意图也触发 - 关键词预筛选避免无关 embedding 调用 - persona + synthesizer 三重诚实规则 - 工具结果持久化到会话历史 **平台桥接器** - detached:true Go进程独立存活 - ethend 重启自动接管已运行服务 - stop() 接管模式 taskkill/F/ PID - Windows netstat 替代 fuser 获取 PID - 重复适配器种子逻辑修复 - 失败转发日志 Direction: error **崩溃诊断** - crashlog 包 (Recover + WrapHTTP + LLMCall) - /api/v1/debug/goroutines 端点 - thinker 操作日志 + 30s stats - 日志写入 logs/ 目录持久化 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -185,7 +185,7 @@ func main() {
|
||||
if err != nil {
|
||||
msgLogger.Log(logging.LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Direction: "outgoing",
|
||||
Direction: "error",
|
||||
Platform: msg.Platform,
|
||||
ChannelID: msg.ChannelID,
|
||||
SenderID: msg.OriginalSenderUID,
|
||||
@@ -196,7 +196,14 @@ func main() {
|
||||
}()
|
||||
}
|
||||
|
||||
// 戳一戳/动作消息:总是回复
|
||||
isPoke := strings.Contains(msg.Content, "【动作】")
|
||||
|
||||
switch {
|
||||
case isPoke:
|
||||
msg.RouteType = "normal"
|
||||
response, routeErr = forwardToAICore(cfg, msg, "text", chatUserID, groupSessionID, imageURLs, videoURLs, voiceURLs, isAdmin)
|
||||
|
||||
case isMessageHistorical(msg, router):
|
||||
msg.RouteType = "silent"
|
||||
namespace := buildMemoryNamespace(msg.Platform, msg.ChannelType, msg.ChannelID)
|
||||
@@ -224,10 +231,12 @@ func main() {
|
||||
response = &bridge.UnifiedResponse{Messages: []bridge.ResponseMessage{{DisplayType: "silent"}}, Platform: msg.Platform}
|
||||
|
||||
case isSilent:
|
||||
msg.RouteType = "silent"
|
||||
// 群聊环境消息:让 LLM 自己判断是否值得插话
|
||||
msg.RouteType = "group_ambient"
|
||||
// 同时后台记录(记忆提取)
|
||||
namespace := buildMemoryNamespace(msg.Platform, msg.ChannelType, msg.ChannelID)
|
||||
fireSilent(namespace, imageURLs, videoURLs, voiceURLs)
|
||||
response = &bridge.UnifiedResponse{Messages: []bridge.ResponseMessage{{DisplayType: "silent"}}, Platform: msg.Platform}
|
||||
response, routeErr = forwardToAICore(cfg, msg, "group_ambient", chatUserID, groupSessionID, imageURLs, videoURLs, voiceURLs, isAdmin)
|
||||
|
||||
default:
|
||||
msg.RouteType = "normal"
|
||||
@@ -237,7 +246,7 @@ func main() {
|
||||
if routeErr != nil {
|
||||
msgLogger.Log(logging.LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Direction: "outgoing",
|
||||
Direction: "error",
|
||||
Platform: msg.Platform,
|
||||
ChannelID: msg.ChannelID,
|
||||
SenderID: msg.OriginalSenderUID,
|
||||
@@ -282,9 +291,13 @@ func main() {
|
||||
mux := http.NewServeMux()
|
||||
bh := handler.NewBridgeHandler(router)
|
||||
bh.SetLogFunc(func(platform, channelID, senderID, content string, success bool) {
|
||||
dir := "outgoing"
|
||||
if !success {
|
||||
dir = "error"
|
||||
}
|
||||
msgLogger.Log(logging.LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Direction: "outgoing",
|
||||
Direction: dir,
|
||||
Platform: platform,
|
||||
ChannelID: channelID,
|
||||
SenderID: senderID,
|
||||
@@ -439,6 +452,14 @@ func startOBv11Readers(router *bridge.PlatformRouter) {
|
||||
toSend = append(toSend, rm)
|
||||
}
|
||||
}
|
||||
// NapCat 输入状态:私聊时在发送前显示"正在输入"
|
||||
if messageType == "private" {
|
||||
if cur, err := router.GetAdapter(adapterKey); err == nil {
|
||||
if qa, ok := cur.(*qqadapter.Adapter); ok {
|
||||
qa.SetTypingStatus(userID, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
interval := time.Duration(adapter.SendIntervalMs()) * time.Millisecond
|
||||
if interval <= 0 {
|
||||
interval = 2 * time.Second
|
||||
@@ -470,6 +491,14 @@ func startOBv11Readers(router *bridge.PlatformRouter) {
|
||||
fmt.Printf("[qq:%s] send msg error: %v\n", adapterKey, sendErr)
|
||||
}
|
||||
}
|
||||
// NapCat: clear typing indicator
|
||||
if messageType == "private" {
|
||||
if cur, err := router.GetAdapter(adapterKey); err == nil {
|
||||
if qa, ok := cur.(*qqadapter.Adapter); ok {
|
||||
qa.SetTypingStatus(userID, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -501,13 +530,17 @@ func createAdapters(cfg *config.Config, store *config.Store) []bridge.PlatformAd
|
||||
}
|
||||
|
||||
// Seed default adapters for platforms that have no stored config.
|
||||
// Track platform types (not config names) to avoid duplicate adapters.
|
||||
seededTypes := map[string]bool{}
|
||||
for _, a := range adapters {
|
||||
seededTypes[a.PlatformName()] = true
|
||||
}
|
||||
for _, stored := range store.List() {
|
||||
seededTypes[stored.Platform] = true // stored.Platform is the type (e.g. "obv11")
|
||||
}
|
||||
defaultPlatforms := []string{"obv11", "telegram", "webhook", "wechat", "feishu", "discord"}
|
||||
for _, name := range defaultPlatforms {
|
||||
if seen[name] || seededTypes[name] {
|
||||
if seededTypes[name] {
|
||||
continue
|
||||
}
|
||||
fields := mergeFields(cfg, name, nil)
|
||||
@@ -548,7 +581,14 @@ func createSingleAdapter(cfg *config.Config, platform, configName, configID stri
|
||||
sendIntervalMs = n
|
||||
}
|
||||
}
|
||||
return qqadapter.NewAdapter(configID, configName, mode, port, token, remoteURL, sendIntervalMs)
|
||||
adapter := qqadapter.NewAdapter(configID, configName, mode, port, token, remoteURL, sendIntervalMs)
|
||||
// Optional HTTP API configuration (for typing status, etc.)
|
||||
httpURL := fields["http_url"]
|
||||
httpToken := fields["http_token"]
|
||||
if httpURL != "" || httpToken != "" {
|
||||
adapter.SetHTTPConfig(httpURL, httpToken)
|
||||
}
|
||||
return adapter
|
||||
case "telegram":
|
||||
token := cfg.TelegramToken
|
||||
if t, ok := fields["bot_token"]; ok && t != "" {
|
||||
|
||||
Reference in New Issue
Block a user