feat(realtime): complete fake channel reply flow

This commit is contained in:
2026-07-13 14:57:28 +08:00
parent a712b91982
commit d16bb55d91
24 changed files with 1441 additions and 840 deletions
@@ -4,12 +4,14 @@ import (
"context"
"errors"
"io"
"net"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/llm"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/pkg/pagination"
@@ -1055,6 +1057,29 @@ func handleServiceError(c *gin.Context, err error) {
return
}
errMsg := err.Error()
if errors.Is(err, llm.ErrProviderNotConfigured) {
response.AbortWithStatusError(c, http.StatusServiceUnavailable, response.ErrCopilotNotConfigured, errMsg)
return
}
var providerErr *llm.APIError
if errors.As(err, &providerErr) {
switch providerErr.StatusCode {
case http.StatusUnauthorized, http.StatusForbidden:
response.AbortWithStatusError(c, http.StatusBadGateway, response.ErrCopilotProviderAuth, "Copilot provider authentication failed")
case http.StatusNotFound:
response.AbortWithStatusError(c, http.StatusBadGateway, response.ErrCopilotModelNotFound, "Copilot provider endpoint or model was not found")
case http.StatusTooManyRequests:
response.AbortWithStatusError(c, http.StatusTooManyRequests, response.ErrCopilotProviderRateLimited, "Copilot provider rate limit exceeded")
default:
response.AbortWithStatusError(c, http.StatusBadGateway, response.ErrCopilotProviderUnreachable, "Copilot provider request failed")
}
return
}
var networkErr net.Error
if errors.Is(err, context.DeadlineExceeded) || (errors.As(err, &networkErr) && networkErr.Timeout()) {
response.AbortWithStatusError(c, http.StatusGatewayTimeout, response.ErrCopilotProviderTimeout, "Copilot provider request timed out")
return
}
lower := strings.ToLower(errMsg)
if strings.Contains(lower, "not found") || strings.Contains(lower, "record not found") {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, errMsg)
@@ -694,6 +694,7 @@ func serializeContactWithContext(ctx context.Context, contact *model.Contact) ma
"custom_attributes": jsonObject(contact.CustomAttributes),
"last_activity_at": int64Value(contact.LastActivityAt),
"created_at": contact.CreatedAt.Unix(),
"type": "contact",
}
}
@@ -710,6 +711,7 @@ func serializeUser(user *model.User, accountID uint) map[string]any {
"name": user.Name,
"role": nonEmpty(user.Role, "agent"),
"thumbnail": user.AvatarURL,
"type": "user",
}
if attrs := jsonObject(user.CustomAttributes); len(attrs) > 0 {
payload["custom_attributes"] = attrs
@@ -390,9 +390,19 @@ func TestSerializeContactUsesPresenceStatus(t *testing.T) {
payload := serializeContactWithContext(ctx, contact)
require.Equal(t, "online", payload["availability_status"])
require.Equal(t, "contact", payload["type"])
require.Equal(t, "offline", serializeContact(contact)["availability_status"])
}
func TestSerializeUserIncludesSenderType(t *testing.T) {
user := &model.User{AccountID: 7, Name: "Support Agent"}
user.ID = 43
payload := serializeUser(user, user.AccountID)
require.Equal(t, "user", payload["type"])
}
func TestSerializeCRMContactUsesPresenceStatus(t *testing.T) {
contact := &model.Contact{AccountID: 9, Name: "CRM Online Contact"}
contact.ID = 99