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:
2026-06-28 11:29:29 +08:00
parent 0d6970a2d3
commit fd44b15d81
23 changed files with 1447 additions and 254 deletions
@@ -39,6 +39,7 @@ type Orchestrator struct {
msgScheduler *scheduler.MessageScheduler
emotionTracker *persona.EmotionTracker
toolRegistry *plgManager.ToolRegistry
traceFn func(hop, sessionID, userID, label, status, detail string, durationMs int64) // trace 回调
visionProvider llm.LLMProvider // 视觉模型 (图片预处理)
ocrProvider llm.LLMProvider // OCR 模型 (文字提取,与视觉模型并行调用)
videoProvider llm.LLMProvider // 视频模型 (短视频理解)
@@ -83,10 +84,16 @@ func (o *Orchestrator) SetToolRegistry(tr *plgManager.ToolRegistry) {
}
// SetToolResultPusher sets the callback for proactive tool result delivery.
func (o *Orchestrator) SetToolResultPusher(pusher func(sessionID, userID, toolName, result string)) {
func (o *Orchestrator) SetToolResultPusher(pusher func(sessionID, userID, toolName, result string, params SynthesizeParams)) {
o.synthesizer.SetResultPusher(pusher)
}
// SetTraceFunc sets the trace callback for pipeline event recording.
func (o *Orchestrator) SetTraceFunc(fn func(hop, sessionID, userID, label, status, detail string, durationMs int64)) {
o.traceFn = fn
o.synthesizer.SetTraceFunc(fn)
}
// SetVisionProvider sets the vision model provider for image preprocessing.
func (o *Orchestrator) SetVisionProvider(vp llm.LLMProvider) {
o.visionProvider = vp
@@ -191,11 +198,24 @@ func (o *Orchestrator) ProcessInput(
isCoSession := false
o.sessionProcMu.Lock()
if o.sessionProc[params.SessionID] {
// 等待主会话释放(最多等 3s,避免永久死锁)
waitStart := time.Now()
for o.activeCoSessions[params.SessionID] >= o.maxCoSessions && time.Since(waitStart) < 3*time.Second {
o.sessionProcMu.Unlock()
select {
case <-ctx.Done():
o.sessionProcMu.Lock()
o.sessionProcMu.Unlock()
logger.Printf("[orchestrator] 等待会话释放时 context 取消")
return
case <-time.After(500 * time.Millisecond):
}
o.sessionProcMu.Lock()
}
if o.activeCoSessions[params.SessionID] >= o.maxCoSessions {
o.sessionProcMu.Unlock()
logger.Printf("[orchestrator] 协会议话已达上限,排队等待")
time.Sleep(500 * time.Millisecond)
o.sessionProcMu.Lock()
logger.Printf("[orchestrator] 协会议话已达上限且等待超时,拒绝请求")
return
}
o.activeCoSessions[params.SessionID]++
isCoSession = true
@@ -291,6 +311,9 @@ func (o *Orchestrator) ProcessInput(
}
}
logger.Printf("[orchestrator] 意图分析耗时: %v, primary=%s", time.Since(startTime), intent.Primary)
if o.traceFn != nil {
o.traceFn("intent", params.SessionID, params.UserID, "🎯 "+intent.Primary, "success", intent.Primary, time.Since(startTime).Milliseconds())
}
// 1.6 记录情感状态
if o.emotionTracker != nil {
@@ -593,6 +616,10 @@ func (o *Orchestrator) ProcessInput(
logger.Printf("[orchestrator] 处理完成: intent=%s, content_len=%d, time=%v",
intent.Primary, len([]rune(fullContent)), time.Since(startTime))
if o.traceFn != nil {
totalMs := time.Since(startTime).Milliseconds()
o.traceFn("response", params.SessionID, params.UserID, "💬 回复", "success", fmt.Sprintf("len=%d", len([]rune(fullContent))), totalMs)
}
}()
return eventCh, nil
@@ -19,7 +19,8 @@ import (
type Synthesizer struct {
llmAdapter *llm.Adapter
toolRegistry *plgManager.ToolRegistry
resultPusher func(sessionID, userID, toolName, result string)
resultPusher func(sessionID, userID, toolName, result string, params SynthesizeParams)
traceFn func(hop, sessionID, userID, label, status, detail string, durationMs int64)
}
// NewSynthesizer 创建综合器
@@ -31,10 +32,15 @@ func NewSynthesizer(llmAdapter *llm.Adapter, toolRegistry *plgManager.ToolRegist
}
// SetResultPusher sets the callback for proactive tool result delivery.
func (s *Synthesizer) SetResultPusher(pusher func(sessionID, userID, toolName, result string)) {
func (s *Synthesizer) SetResultPusher(pusher func(sessionID, userID, toolName, result string, params SynthesizeParams)) {
s.resultPusher = pusher
}
// SetTraceFunc sets the trace callback.
func (s *Synthesizer) SetTraceFunc(fn func(hop, sessionID, userID, label, status, detail string, durationMs int64)) {
s.traceFn = fn
}
// SynthesizeParams 综合参数
type SynthesizeParams struct {
UserID string
@@ -80,6 +86,11 @@ func (s *Synthesizer) Synthesize(ctx context.Context, params SynthesizeParams, e
for round := 0; len(resp.ToolCalls) > 0 && round < maxRounds; round++ {
logger.Printf("[synthesizer] LLM 请求 %d 个工具调用 (round=%d)", len(resp.ToolCalls), round)
for _, tc := range resp.ToolCalls {
if s.traceFn != nil {
s.traceFn("tool_call", params.SessionID, params.UserID, "🔧 "+tc.Name, "running", "", 0)
}
}
messages = append(messages, model.LLMMessage{
Role: model.RoleAssistant,
@@ -114,15 +125,14 @@ func (s *Synthesizer) Synthesize(ctx context.Context, params SynthesizeParams, e
// adapter_name will be resolved when the reminder fires
}
s.emitToolProgress(eventCh, tc.Name, "started", 0, "正在执行 "+tc.Name)
s.emitToolProgress(eventCh, tc.Name, "started", 0, "正在执行 "+tc.Name)
// 工具调用全部异步执行,不阻塞会话
go s.executeAsyncAndStore(tc, args, params.SessionID, eventCh)
// 所有工具异步执行,不阻塞前台会话
go s.executeAsyncAndStore(tc, args, params, eventCh)
result := &plgSDK.ToolResult{
ToolName: tc.Name,
Success: true,
Output: fmt.Sprintf("[后台执行中] %s 正在后台运行,结果稍后返回。", tc.Name),
Output: fmt.Sprintf(`[后台执行中] %s 已提交后台执行。不要猜测或编造结果,告知用户你正在查询中即可。真实结果稍后会发送给你。`, tc.Name),
}
resultJSON, _ := json.Marshal(result)
messages = append(messages, model.LLMMessage{
@@ -176,7 +186,7 @@ func (s *Synthesizer) emitToolProgress(eventCh chan<- model.StreamEvent, name, s
}
// executeAsyncAndStore runs a tool in background and stores the result for the next turn.
func (s *Synthesizer) executeAsyncAndStore(tc model.ToolCall, args map[string]interface{}, sessionID string, eventCh chan<- model.StreamEvent) {
func (s *Synthesizer) executeAsyncAndStore(tc model.ToolCall, args map[string]interface{}, params SynthesizeParams, eventCh chan<- model.StreamEvent) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
@@ -188,20 +198,27 @@ func (s *Synthesizer) executeAsyncAndStore(tc model.ToolCall, args map[string]in
}
s.emitToolProgress(eventCh, tc.Name, "completed", 1.0, tc.Name+" 后台执行完成")
if s.traceFn != nil {
status := "success"
if result == nil || !result.Success {
status = "error"
}
s.traceFn("tool_call", params.SessionID, params.UserID, "🔧 "+tc.Name, status, result.Output, time.Since(time.Now()).Milliseconds())
}
resultJSON, _ := json.Marshal(result)
store := GetGlobalPendingToolStore()
if store != nil {
store.AppendToolResult(sessionID, PendingToolResult{
store.AppendToolResult(params.SessionID, PendingToolResult{
ToolCallID: tc.ID,
ToolName: tc.Name,
Result: string(resultJSON),
Success: result != nil && result.Success,
})
}
// 主动推送工具结果
// 触发工具跟进回调 — 由 main.go 驱动 LLM 生成回复并推送到原渠道
if s.resultPusher != nil && result != nil && result.Success {
s.resultPusher(sessionID, "", tc.Name, string(resultJSON))
s.resultPusher(params.SessionID, params.UserID, tc.Name, string(resultJSON), params)
}
}
@@ -263,7 +280,7 @@ func (s *Synthesizer) buildSynthesizeMessages(params SynthesizeParams) []model.L
if params.KnowledgeInfo != "" && !strings.Contains(params.KnowledgeInfo, "未找到") {
messages = append(messages, model.LLMMessage{
Role: model.RoleSystem,
Content: fmt.Sprintf("【知识库参考资料】\n%s", params.KnowledgeInfo),
Content: fmt.Sprintf("【知识库参考资料 - 必须严格基于以下内容回答,不得编造、不得虚构、不得猜测。如果资料中没有直接答案,使用 web_search 工具搜索后再回答,不要自己编。】\n%s", params.KnowledgeInfo),
})
}