From ea327e2d97b7b7c06a8a3e691b31b6d5e29d99f9 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Fri, 10 Apr 2026 16:03:06 +0800 Subject: [PATCH] feat: refactor handoff logic and update UI descriptions for Graph Tool integration --- internal/ai/runtime/reply_service.go | 37 +------------------ internal/ai/runtime/service.go | 19 +++++++++- .../(console)/ai-agents/_components/edit.tsx | 8 ++-- 3 files changed, 22 insertions(+), 42 deletions(-) diff --git a/internal/ai/runtime/reply_service.go b/internal/ai/runtime/reply_service.go index 7d58976..493fe70 100644 --- a/internal/ai/runtime/reply_service.go +++ b/internal/ai/runtime/reply_service.go @@ -103,14 +103,6 @@ func (s *aiReplyService) TriggerReply(ctx context.Context, conversation models.C if pendingInterrupt := svc.ConversationInterruptService.FindLatestPendingByConversationID(conversation.ID); pendingInterrupt != nil { return s.resumePendingInterrupt(ctx, conversation, message, aiAgent, pendingInterrupt, trace, &summary) } - if s.shouldHandoffByQuestion(message.Content, aiAgent) { - return s.handoffConversation(conversation, aiAgent, "用户主动要求人工") - } - if aiAgent.ServiceMode != enums.IMConversationServiceModeAIOnly && - aiAgent.MaxAIReplyRounds > 0 && - conversation.AIReplyRounds >= aiAgent.MaxAIReplyRounds { - return s.handoffConversation(conversation, aiAgent, "达到AI最大回复轮次") - } aiConfig := svc.AIConfigService.Get(aiAgent.AIConfigID) if aiConfig == nil { return fmt.Errorf("ai config is nil") @@ -295,23 +287,6 @@ func (s *aiReplyService) sendAIReply(conversation models.Conversation, message m return replyMessage, err } -func (s *aiReplyService) shouldHandoffByQuestion(question string, aiAgent models.AIAgent) bool { - if aiAgent.ServiceMode == enums.IMConversationServiceModeAIOnly { - return false - } - normalized := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(question)), " ", "") - if normalized == "" { - return false - } - keywords := []string{"转人工", "人工客服"} - for _, keyword := range keywords { - if strings.Contains(normalized, keyword) { - return true - } - } - return false -} - func (s *aiReplyService) writeRunLog(startedAt time.Time, message models.Message, conversation models.Conversation, aiAgent models.AIAgent, question string, runErr error, trace *aiReplyTraceData, summary *Summary) { errorMessage := "" @@ -661,7 +636,7 @@ func parseRuntimeTraceData(raw string) runtimeTraceProjection { func isCancellationReply(replyText string) bool { replyText = strings.TrimSpace(replyText) - return strings.Contains(replyText, "已取消本次工单创建") + return strings.Contains(replyText, "已取消本次工单创建") || strings.Contains(replyText, "已取消本次转人工") } func isCheckpointMissingError(err error) bool { @@ -672,16 +647,6 @@ func isCheckpointMissingError(err error) bool { return strings.Contains(message, "failed to load from checkpoint") && strings.Contains(message, "not exist") } -func (s *aiReplyService) handoffConversation(conversation models.Conversation, aiAgent models.AIAgent, reason string) error { - if err := svc.ConversationService.HandoffByAI(conversation.ID, &aiAgent, reason); err != nil { - return err - } - if _, err := svc.MessageService.SendAIMessage(conversation.ID, aiAgent.ID, fmt.Sprintf("ai_handoff_%d", conversation.LastMessageID), enums.IMMessageTypeText, "已为你转接人工客服,请稍候。", "", s.buildAIPrincipal(aiAgent)); err != nil { - return err - } - return nil -} - func (s *aiReplyService) buildAIPrincipal(aiAgent models.AIAgent) *dto.AuthPrincipal { username := "AI" if strings.TrimSpace(aiAgent.Name) != "" { diff --git a/internal/ai/runtime/service.go b/internal/ai/runtime/service.go index b6a0a09..fbb5eda 100644 --- a/internal/ai/runtime/service.go +++ b/internal/ai/runtime/service.go @@ -95,7 +95,7 @@ func (s *service) prepareToolsForRun(req *Request) error { AIAgent: req.AIAgent, AIConfig: req.AIConfig, UserMessage: req.UserMessage, - AllowedToolCodes: resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill), + AllowedToolCodes: ensureCoreGraphToolCodes(resolveAllowedToolCodes(req.AIAgent, req.SelectedSkill)), }) if err != nil { return err @@ -113,7 +113,7 @@ func (s *service) prepareToolsForResume(req *ResumeRequest) error { Conversation: req.Conversation, AIAgent: req.AIAgent, AIConfig: req.AIConfig, - AllowedToolCodes: parseAgentAllowedToolCodes(req.AIAgent), + AllowedToolCodes: ensureCoreGraphToolCodes(parseAgentAllowedToolCodes(req.AIAgent)), }) if err != nil { return err @@ -281,3 +281,18 @@ func resolveAllowedToolCodes(aiAgent *models.AIAgent, skill *models.SkillDefinit return ret } } + +func ensureCoreGraphToolCodes(toolCodes []string) []string { + if len(toolCodes) == 0 { + return nil + } + ret := make([]string, 0, len(toolCodes)+1) + ret = append(ret, toolCodes...) + for _, item := range ret { + if strings.TrimSpace(item) == toolx.GraphHandoffConversationToolCode { + return ret + } + } + ret = append(ret, toolx.GraphHandoffConversationToolCode) + return ret +} diff --git a/web/app/(console)/ai-agents/_components/edit.tsx b/web/app/(console)/ai-agents/_components/edit.tsx index 4358c06..aa1d69b 100644 --- a/web/app/(console)/ai-agents/_components/edit.tsx +++ b/web/app/(console)/ai-agents/_components/edit.tsx @@ -124,7 +124,7 @@ const fallbackModeOptions = getEnumOptions(AIAgentFallbackModeLabels).map( value: String(option.value), label: option.label, }), -); +).filter((option) => option.value !== String(AIAgentFallbackMode.Handoff)); function buildForm(item: AIAgent | null): EditForm { if (!item) { @@ -973,7 +973,7 @@ function EditDialogBody({
@@ -1050,7 +1050,7 @@ function EditDialogBody({ /> - 单个会话内 AI 成功回复达到该次数后,下一条客户消息会自动转人工。填 0 表示不限制。 + 该字段不再触发系统自动转人工,当前仅保留为兼容配置。转人工统一通过 AI 的 handoff_to_human Graph Tool 执行。 @@ -1163,7 +1163,7 @@ function EditDialogBody({
- 仅在兜底模式为“直接声明无答案”或“引导补充信息”时使用;如果兜底模式配置为转人工,这里的文案不会生效。 + 仅在兜底模式为“直接声明无答案”或“引导补充信息”时使用。转人工已统一改为 AI Graph Tool,不再通过兜底模式直接触发。