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:
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user