refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -2,17 +2,30 @@ package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.tczkiot.com/wlw/ai-agent/contract"
|
||||
applicationruntime "code.tczkiot.com/wlw/ai-agent/internal/ai/application/runtime"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
|
||||
"code.tczkiot.com/wlw/ai-agent/internal/pkg/tracex"
|
||||
svc "code.tczkiot.com/wlw/ai-agent/internal/services"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
const aiReplyFailedReply = "已收到您的消息,但本次智能处理没有完成。请稍后重试,或回复“人工客服”继续处理。"
|
||||
|
||||
const businessIdentityMenuPrefix = "identity_menu"
|
||||
|
||||
const aiReplyInvocationToolCode = "runtime/ai_reply"
|
||||
|
||||
const aiReplyInvocationRecoveryGrace = 30 * time.Second
|
||||
|
||||
func (s *aiReplyService) resolveReplyTimeout(aiAgent models.AIAgent) time.Duration {
|
||||
if aiAgent.ReplyTimeoutSeconds <= 0 {
|
||||
return time.Duration(defaultAIReplyAsyncTimeoutSeconds) * time.Second
|
||||
@@ -23,27 +36,265 @@ func (s *aiReplyService) resolveReplyTimeout(aiAgent models.AIAgent) time.Durati
|
||||
return time.Duration(aiAgent.ReplyTimeoutSeconds) * time.Second
|
||||
}
|
||||
|
||||
func (s *aiReplyService) TriggerReplyAsync(conversation models.Conversation, message models.Message) {
|
||||
go func() {
|
||||
aiAgent := svc.AIAgentService.Get(conversation.AIAgentID)
|
||||
if aiAgent == nil || aiAgent.Status != enums.StatusOk {
|
||||
return
|
||||
func (s *aiReplyService) TriggerReplyAsync(requestContext context.Context, conversation models.Conversation, message models.Message) {
|
||||
aiAgent := svc.AIAgentService.Get(conversation.AIAgentID)
|
||||
if aiAgent == nil || aiAgent.Status != enums.StatusOk {
|
||||
return
|
||||
}
|
||||
timeout := s.resolveReplyTimeout(*aiAgent)
|
||||
invocationKey := fmt.Sprintf("message:%d:revision:%d", message.ID, aiAgent.PublishedRevisionID)
|
||||
claim, err := svc.AgentToolInvocationService.ClaimRecoverable(
|
||||
conversation.ID,
|
||||
aiAgent.ID,
|
||||
aiReplyInvocationToolCode,
|
||||
invocationKey,
|
||||
time.Now().Add(-(timeout + aiReplyInvocationRecoveryGrace)),
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("failed to claim ai reply run",
|
||||
"requestId", message.RequestID,
|
||||
"conversation_id", conversation.ID,
|
||||
"message_id", message.ID,
|
||||
"revision_id", aiAgent.PublishedRevisionID,
|
||||
"error", err)
|
||||
return
|
||||
}
|
||||
if claim == nil || claim.Item == nil || !claim.Acquired {
|
||||
return
|
||||
}
|
||||
if committedAIReply(conversation.ID, message.ID) != nil {
|
||||
if err := svc.AgentToolInvocationService.Complete(claim.Item, `{}`); err != nil {
|
||||
slog.Error("failed to reconcile recovered ai reply claim", "conversation_id", conversation.ID, "message_id", message.ID, "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
proofContext := contract.BindCustomerAccessProofToMessage(requestContext, conversation.ID, message.ID, message.RequestID)
|
||||
proof, hasProof := contract.CustomerAccessProofFromContext(proofContext)
|
||||
go func() {
|
||||
startedAt := time.Now()
|
||||
timeout := s.resolveReplyTimeout(*aiAgent)
|
||||
ctx, cancel := context.WithTimeout(tracex.ContextWithRequestID(context.Background(), message.RequestID), timeout)
|
||||
ctx := tracex.ContextWithRequestID(context.Background(), message.RequestID)
|
||||
if hasProof {
|
||||
ctx = contract.WithCustomerAccessProof(ctx, proof)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
if err := s.TriggerReply(ctx, conversation, message, *aiAgent); err != nil {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
err := fmt.Errorf("ai reply panic: %v", recovered)
|
||||
_ = svc.AgentToolInvocationService.FailRetryable(claim.Item, err)
|
||||
slog.Error("panic while triggering ai reply",
|
||||
"requestId", message.RequestID,
|
||||
"conversation_id", conversation.ID,
|
||||
"message_id", message.ID,
|
||||
"error", err)
|
||||
s.commitFailureReplyIfNeeded(conversation, message, *aiAgent, err)
|
||||
}
|
||||
}()
|
||||
var triggerErr error
|
||||
if s.triggerReply != nil {
|
||||
triggerErr = s.triggerReply(ctx, conversation, message, *aiAgent)
|
||||
} else {
|
||||
triggerErr = s.TriggerReply(ctx, conversation, message, *aiAgent)
|
||||
}
|
||||
if triggerErr != nil {
|
||||
_ = svc.AgentToolInvocationService.FailRetryable(claim.Item, triggerErr)
|
||||
slog.Error("failed to trigger ai reply",
|
||||
"requestId", message.RequestID,
|
||||
"message_id", message.ID,
|
||||
"timeout_ms", timeout.Milliseconds(),
|
||||
"elapsed_ms", time.Since(startedAt).Milliseconds(),
|
||||
"error", triggerErr)
|
||||
s.commitFailureReplyIfNeeded(conversation, message, *aiAgent, triggerErr)
|
||||
return
|
||||
}
|
||||
if err := svc.AgentToolInvocationService.Complete(claim.Item, `{}`); err != nil {
|
||||
slog.Error("failed to complete ai reply run claim",
|
||||
"requestId", message.RequestID,
|
||||
"conversation_id", conversation.ID,
|
||||
"message_id", message.ID,
|
||||
"error", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func committedAIReply(conversationID, messageID int64) *models.Message {
|
||||
for _, prefix := range []string{"ai_reply", "identity_prompt", "ai_interrupt", "ai_interrupt_expired", "ai_resume", "ai_interrupt_resume"} {
|
||||
clientMsgID := fmt.Sprintf("%s_%d", prefix, messageID)
|
||||
if existing := svc.MessageService.FindOne(sqls.NewCnd().Eq("conversation_id", conversationID).Eq("client_msg_id", clientMsgID)); existing != nil {
|
||||
return existing
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *aiReplyService) commitFailureReplyIfNeeded(conversation models.Conversation, message models.Message, aiAgent models.AIAgent, cause error) {
|
||||
clientMsgID := fmt.Sprintf("ai_error_%d", message.ID)
|
||||
if existing := svc.MessageService.FindOne(
|
||||
sqls.NewCnd().Eq("conversation_id", conversation.ID).Eq("client_msg_id", clientMsgID),
|
||||
); existing != nil {
|
||||
return
|
||||
}
|
||||
if _, err := s.commit.CommitAIReply(replyCommitInput{
|
||||
Conversation: conversation,
|
||||
Message: message,
|
||||
AIAgent: aiAgent,
|
||||
ReplyText: aiReplyFailureText(cause),
|
||||
ClientPrefix: "ai_error",
|
||||
}); err != nil {
|
||||
slog.Error("failed to commit ai error reply",
|
||||
"requestId", message.RequestID,
|
||||
"conversation_id", conversation.ID,
|
||||
"message_id", message.ID,
|
||||
"error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func aiReplyFailureText(cause error) string {
|
||||
if cause == nil {
|
||||
return aiReplyFailedReply
|
||||
}
|
||||
message := strings.ToLower(cause.Error())
|
||||
switch {
|
||||
case strings.Contains(message, "ai 请求标识未设置"),
|
||||
strings.Contains(message, "invalid_ai_request_id"):
|
||||
return "系统内置 AI 请求失败:AI 请求标识无效。请联系管理员检查网关配置,或回复“人工客服”继续处理。"
|
||||
case strings.Contains(message, "insufficient_ai_balance"),
|
||||
strings.Contains(message, "ai 额度不足"):
|
||||
return "系统内置 AI 额度不足,请充值后重试,或回复“人工客服”继续处理。"
|
||||
case strings.Contains(message, "invalid_ai_key"),
|
||||
strings.Contains(message, "missing_ai_key"),
|
||||
strings.Contains(message, "ai key 格式无效"),
|
||||
strings.Contains(message, "ai 授权凭证无效"):
|
||||
return "系统内置 AI Key 无效或已撤销,请联系管理员检查客服设置。"
|
||||
case strings.Contains(message, "ai_gateway_not_configured"),
|
||||
strings.Contains(message, "system built-in llm model is not configured"),
|
||||
strings.Contains(message, "系统内置模型尚未配置"):
|
||||
return "系统内置 AI 模型尚未配置,请联系管理员完成配置。"
|
||||
case strings.Contains(message, "context deadline exceeded"),
|
||||
strings.Contains(message, "request timeout"),
|
||||
strings.Contains(message, "client.timeout"):
|
||||
return "系统内置 AI 请求超时,请稍后重试,或回复“人工客服”继续处理。"
|
||||
case strings.Contains(message, "internal_error"),
|
||||
strings.Contains(message, "网关内部异常"):
|
||||
if requestID := extractAIGatewayRequestID(cause.Error()); requestID != "" {
|
||||
return "系统内置 AI 网关内部异常,请稍后重试;排查编号:" + requestID + "。如仍失败,请联系管理员或回复“人工客服”。"
|
||||
}
|
||||
return "系统内置 AI 网关内部异常,请稍后重试;如仍失败,请联系管理员或回复“人工客服”。"
|
||||
case isAIUpstreamError(message):
|
||||
return aiUpstreamFailureText(message)
|
||||
default:
|
||||
return aiReplyFailedReply
|
||||
}
|
||||
}
|
||||
|
||||
func extractAIGatewayRequestID(message string) string {
|
||||
lowerMessage := strings.ToLower(message)
|
||||
for _, marker := range []string{"request_id:", "request_id="} {
|
||||
start := strings.Index(lowerMessage, marker)
|
||||
if start < 0 {
|
||||
continue
|
||||
}
|
||||
value := strings.TrimSpace(message[start+len(marker):])
|
||||
value = strings.TrimLeft(value, "(\"'")
|
||||
end := 0
|
||||
for end < len(value) {
|
||||
char := value[end]
|
||||
if (char >= 'a' && char <= 'z') ||
|
||||
(char >= 'A' && char <= 'Z') ||
|
||||
(char >= '0' && char <= '9') || char == '-' || char == '_' {
|
||||
end++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if end > 0 {
|
||||
return value[:end]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isAIUpstreamError(message string) bool {
|
||||
if strings.Contains(message, "ai_upstream_failed") || strings.Contains(message, "ai_request_failed") {
|
||||
return true
|
||||
}
|
||||
providerMentioned := strings.Contains(message, "qwen") ||
|
||||
strings.Contains(message, "千问") ||
|
||||
strings.Contains(message, "deepseek") ||
|
||||
strings.Contains(message, "dashscope")
|
||||
if !providerMentioned {
|
||||
return false
|
||||
}
|
||||
for _, marker := range []string{
|
||||
"status code", "bad request", "unauthorized", "forbidden", "too many requests",
|
||||
"returned 4", "returned 5", "返回 4", "返回 5", "invalidparameter",
|
||||
"invalid_parameter", "throttling", "arrearage", "accessdenied", "error",
|
||||
} {
|
||||
if strings.Contains(message, marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func aiUpstreamFailureText(message string) string {
|
||||
provider := "上游模型"
|
||||
if strings.Contains(message, "qwen") || strings.Contains(message, "千问") || strings.Contains(message, "dashscope") {
|
||||
provider = "千问"
|
||||
} else if strings.Contains(message, "deepseek") {
|
||||
provider = "DeepSeek"
|
||||
}
|
||||
switch {
|
||||
case strings.Contains(message, "model not exist"),
|
||||
strings.Contains(message, "model_not_found"),
|
||||
strings.Contains(message, "invalid model"),
|
||||
strings.Contains(message, "model.accessdenied"),
|
||||
strings.Contains(message, "model access denied"),
|
||||
strings.Contains(message, "模型不存在"):
|
||||
return "系统内置 AI 调用失败:" + provider + "模型不存在或暂不可用,也可能尚未开通,请联系管理员检查模型名称和开通状态。"
|
||||
case strings.Contains(message, "authentication"),
|
||||
strings.Contains(message, "invalid api key"),
|
||||
strings.Contains(message, "invalid_api_key"),
|
||||
strings.Contains(message, "invalidapikey"),
|
||||
strings.Contains(message, "unauthorized"):
|
||||
return "系统内置 AI 调用失败:" + provider + " API Key 无效或无权限,请联系管理员检查官网模型配置。"
|
||||
case strings.Contains(message, "rate limit"),
|
||||
strings.Contains(message, "rate_limit"),
|
||||
strings.Contains(message, "too many requests"),
|
||||
strings.Contains(message, "throttling"):
|
||||
return "系统内置 AI 调用失败:" + provider + "请求过于频繁,请稍后重试。"
|
||||
case strings.Contains(message, "insufficient balance"),
|
||||
strings.Contains(message, "insufficient quota"),
|
||||
strings.Contains(message, "arrearage"),
|
||||
strings.Contains(message, "quota"):
|
||||
return "系统内置 AI 调用失败:" + provider + "账户额度不足或已欠费,请联系管理员处理。"
|
||||
case strings.Contains(message, "maximum context length"),
|
||||
strings.Contains(message, "context_length"),
|
||||
strings.Contains(message, "input length"),
|
||||
strings.Contains(message, "tokens exceed"),
|
||||
strings.Contains(message, "too many tokens"):
|
||||
return "系统内置 AI 调用失败:" + provider + "请求内容超出模型上下文长度,请缩短消息后重试。"
|
||||
case strings.Contains(message, "does not support tools"),
|
||||
strings.Contains(message, "tool calling is not supported"),
|
||||
strings.Contains(message, "function calling is not supported"),
|
||||
strings.Contains(message, "unsupported tool"),
|
||||
strings.Contains(message, "unsupported function"):
|
||||
return "系统内置 AI 调用失败:当前" + provider + "模型不支持客服工具调用,请联系管理员更换可用模型。"
|
||||
case strings.Contains(message, "data_inspection_failed"),
|
||||
strings.Contains(message, "content_filter"),
|
||||
strings.Contains(message, "inappropriate content"):
|
||||
return "系统内置 AI 调用失败:" + provider + "拒绝了本次内容,请调整表述后重试。"
|
||||
case strings.Contains(message, "invalidparameter"),
|
||||
strings.Contains(message, "invalid_parameter"),
|
||||
strings.Contains(message, "bad request"),
|
||||
strings.Contains(message, "返回 400"),
|
||||
strings.Contains(message, "returned 400"):
|
||||
return "系统内置 AI 调用失败:" + provider + "请求参数不兼容,请联系管理员检查模型与客服工具配置。"
|
||||
default:
|
||||
return "系统内置 AI 调用失败:" + provider + "服务返回错误,请联系管理员在 AI 回复记录中查看详细原因。"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *aiReplyService) TriggerReply(ctx context.Context, conversation models.Conversation, message models.Message, aiAgent models.AIAgent) (retErr error) {
|
||||
var summary *applicationruntime.RunResult
|
||||
replyCtx := aiReplyContext{
|
||||
@@ -61,10 +312,174 @@ func (s *aiReplyService) TriggerReply(ctx context.Context, conversation models.C
|
||||
if !IsAIAgentRolloutEligible(conversation, aiAgent, svc.ChannelService.Get(conversation.ChannelID)) {
|
||||
return nil
|
||||
}
|
||||
identityResolution, identityErr := resolveGuestBusinessIdentity(ctx, conversation, message)
|
||||
if identityErr != nil || identityResolution.NeedsPrompt {
|
||||
_, err := s.commit.CommitAIReply(replyCommitInput{
|
||||
Conversation: conversation,
|
||||
Message: message,
|
||||
AIAgent: aiAgent,
|
||||
ReplyText: guestBusinessIdentityPrompt(identityResolution, identityErr),
|
||||
ClientPrefix: "identity_prompt",
|
||||
})
|
||||
return err
|
||||
}
|
||||
replyCtx.Conversation = identityResolution.Conversation
|
||||
if pendingInterrupt := svc.ConversationInterruptService.FindLatestPendingByConversationID(conversation.ID); pendingInterrupt != nil {
|
||||
replyCtx.PendingInterrupt = pendingInterrupt
|
||||
return s.resumePendingInterrupt(ctx, replyCtx)
|
||||
}
|
||||
if identityResolution.CandidateProvided && isBusinessIdentityOnlyMessage(message) {
|
||||
handled, err := s.sendBusinessIdentityMenu(ctx, replyCtx)
|
||||
if handled || err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if selection, ok := businessIdentityMenuSelection(message); ok {
|
||||
latest := latestAIMessage(conversation.ID)
|
||||
if latest != nil && strings.HasPrefix(latest.ClientMsgID, businessIdentityMenuPrefix+"_") {
|
||||
matched, aiMessage, err := svc.CustomerQuickActionService.ExecuteSelectedReply(
|
||||
ctx, &replyCtx.Conversation, selection, message.RequestID, message.ID,
|
||||
)
|
||||
if matched || err != nil {
|
||||
return s.finishQuickActionReply(ctx, replyCtx, matched, aiMessage, err)
|
||||
}
|
||||
}
|
||||
if actionCode, matched := legacyCardMenuActionCode(latest, selection); matched {
|
||||
actionMatched, aiMessage, err := svc.CustomerQuickActionService.ExecuteActionReply(
|
||||
ctx, &replyCtx.Conversation, actionCode, message.RequestID, message.ID,
|
||||
)
|
||||
if actionMatched || err != nil {
|
||||
return s.finishQuickActionReply(ctx, replyCtx, actionMatched, aiMessage, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if matched, err := svc.CustomerQuickActionService.ExecuteMatchedReply(
|
||||
ctx,
|
||||
&replyCtx.Conversation,
|
||||
message.Content,
|
||||
message.RequestID,
|
||||
message.ID,
|
||||
); matched || err != nil {
|
||||
return err
|
||||
}
|
||||
return s.executeReply(ctx, replyCtx)
|
||||
}
|
||||
|
||||
func (s *aiReplyService) sendBusinessIdentityMenu(ctx context.Context, replyCtx aiReplyContext) (bool, error) {
|
||||
actions, err := svc.CustomerQuickActionService.ListForConversation(ctx, &replyCtx.Conversation)
|
||||
if err != nil || len(actions) == 0 {
|
||||
return false, err
|
||||
}
|
||||
objectLabel := "业务对象"
|
||||
switch replyCtx.Conversation.CustomerType {
|
||||
case "card":
|
||||
objectLabel = "卡号"
|
||||
case "device":
|
||||
objectLabel = "设备号"
|
||||
case "mall_user":
|
||||
objectLabel = "商城用户"
|
||||
}
|
||||
var builder strings.Builder
|
||||
builder.WriteString("已识别")
|
||||
builder.WriteString(objectLabel)
|
||||
if identifier := strings.TrimSpace(replyCtx.Conversation.CustomerExternalID); identifier != "" {
|
||||
builder.WriteString(":")
|
||||
builder.WriteString(identifier)
|
||||
}
|
||||
builder.WriteString("。\n\n请回复序号选择需要的服务:")
|
||||
for index, action := range actions {
|
||||
builder.WriteString(fmt.Sprintf("\n%d. %s", index+1, action.Title))
|
||||
}
|
||||
builder.WriteString("\n\n也可以直接输入要咨询的问题。")
|
||||
_, err = svc.MessageService.SendAutomaticServiceMessageWithRequestID(
|
||||
replyCtx.Conversation.ID,
|
||||
fmt.Sprintf("%s_%d", businessIdentityMenuPrefix, replyCtx.Message.ID),
|
||||
builder.String(),
|
||||
replyCtx.Message.RequestID,
|
||||
)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func isBusinessIdentityOnlyMessage(message models.Message) bool {
|
||||
content := businessIdentityMessageContent(message)
|
||||
candidates, _ := businessIdentityCandidates(content)
|
||||
for _, candidate := range candidates {
|
||||
content = strings.ReplaceAll(content, candidate, "")
|
||||
}
|
||||
for _, marker := range []string{"卡号", "卡板", "设备号", "设备", "iccid", "imei"} {
|
||||
content = strings.ReplaceAll(strings.ToLower(content), marker, "")
|
||||
}
|
||||
content = strings.Map(func(r rune) rune {
|
||||
if r == ' ' || r == ' ' || r == '\n' || r == '\r' || r == '\t' || r == ' ' {
|
||||
return -1
|
||||
}
|
||||
switch r {
|
||||
case ':', ':', ',', ',', '。', '.', ';', ';', '-', '_':
|
||||
return -1
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}, content)
|
||||
return content == ""
|
||||
}
|
||||
|
||||
func businessIdentityMenuSelection(message models.Message) (int, bool) {
|
||||
content := strings.TrimSpace(businessIdentityMessageContent(message))
|
||||
selection, err := strconv.Atoi(content)
|
||||
return selection, err == nil && selection > 0
|
||||
}
|
||||
|
||||
func latestAIMessage(conversationID int64) *models.Message {
|
||||
return svc.MessageService.FindOne(sqls.NewCnd().
|
||||
Eq("conversation_id", conversationID).
|
||||
Eq("sender_type", enums.IMSenderTypeAI).
|
||||
Desc("id"))
|
||||
}
|
||||
|
||||
func legacyCardMenuActionCode(message *models.Message, selection int) (string, bool) {
|
||||
if message == nil || selection <= 0 {
|
||||
return "", false
|
||||
}
|
||||
content := businessIdentityMessageContent(*message)
|
||||
if strings.Contains(content, "卡片提示停机") &&
|
||||
strings.Contains(content, "无法上网") &&
|
||||
strings.Contains(content, "无信号") &&
|
||||
strings.Contains(content, "已充值但未恢复") {
|
||||
if selection >= 1 && selection <= 4 {
|
||||
return "card/network_diagnosis", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
if strings.Contains(content, "卡片状态") &&
|
||||
strings.Contains(content, "网络连接") &&
|
||||
strings.Contains(content, "套餐") &&
|
||||
strings.Contains(content, "其他问题") {
|
||||
actions := map[int]string{
|
||||
1: "card/status",
|
||||
2: "card/network_diagnosis",
|
||||
3: "card/package",
|
||||
}
|
||||
code, ok := actions[selection]
|
||||
return code, ok
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (s *aiReplyService) finishQuickActionReply(
|
||||
ctx context.Context,
|
||||
replyCtx aiReplyContext,
|
||||
matched bool,
|
||||
aiMessage string,
|
||||
err error,
|
||||
) error {
|
||||
if err != nil || !matched {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(aiMessage) == "" {
|
||||
return nil
|
||||
}
|
||||
replyCtx.Message.Content = aiMessage
|
||||
replyCtx.Message.MessageType = enums.IMMessageTypeText
|
||||
return s.executeReply(ctx, replyCtx)
|
||||
}
|
||||
|
||||
@@ -98,12 +513,11 @@ func (s *aiReplyService) executeReply(ctx context.Context, replyCtx aiReplyConte
|
||||
}
|
||||
if summary != nil && strings.TrimSpace(summary.ReplyText) != "" {
|
||||
_, err := s.commit.CommitAIReply(replyCommitInput{
|
||||
Conversation: replyCtx.Conversation,
|
||||
Message: replyCtx.Message,
|
||||
AIAgent: replyCtx.AIAgent,
|
||||
ReplyText: summary.ReplyText,
|
||||
ClientPrefix: "ai_reply",
|
||||
WorkflowRunID: summary.WorkflowRunID,
|
||||
Conversation: replyCtx.Conversation,
|
||||
Message: replyCtx.Message,
|
||||
AIAgent: replyCtx.AIAgent,
|
||||
ReplyText: summary.ReplyText,
|
||||
ClientPrefix: "ai_reply",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user