chore: import clean Agent Desk snapshot (2/7)
This commit is contained in:
201
internal/handlers/api/auth_handler.go
Normal file
201
internal/handlers/api/auth_handler.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/config"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/pkg/loginaudience"
|
||||
"agent-desk/internal/services"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Login(ctx *gin.Context) {
|
||||
cfg := config.Current()
|
||||
if !cfg.Auth.IsPasswordLoginEnabled() {
|
||||
httpx.WriteJSON(ctx, errorsx.ForbiddenI18n("error.auth.passwordLoginDisabled"))
|
||||
return
|
||||
}
|
||||
req := request.LoginRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
ret, err := services.AuthService.Login(req, cfg.Auth, ctx.ClientIP(), ctx.GetHeader("User-Agent"))
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, ret)
|
||||
}
|
||||
|
||||
func PublicConfig(ctx *gin.Context) {
|
||||
cfg := config.Current()
|
||||
// Saved branding wins over configuration, so an operator can rename a
|
||||
// deployment from the dashboard without touching the file it is running from.
|
||||
branding := services.ResolveBranding()
|
||||
httpx.WriteJSON(ctx, &response.PublicConfigResponse{
|
||||
Language: cfg.LanguageOrDefault(),
|
||||
CompanyName: branding.CompanyName,
|
||||
CompanyLogoURL: branding.CompanyLogoURL,
|
||||
PasswordLoginEnabled: cfg.Auth.IsPasswordLoginEnabled(),
|
||||
OIDCEnabled: cfg.OIDC.Enabled,
|
||||
})
|
||||
}
|
||||
|
||||
// redirectPortalSSORejected sends a portal-initiated enterprise SSO attempt back
|
||||
// to the portal login with a reason, instead of starting a flow that would hand
|
||||
// a visitor a staff session.
|
||||
func redirectPortalSSORejected(ctx *gin.Context, errorParam string) {
|
||||
ctx.Redirect(
|
||||
http.StatusFound,
|
||||
"/support/login?"+errorParam+"="+url.QueryEscape(i18nx.T(ctx, "error.auth.staffSsoOnPortal")),
|
||||
)
|
||||
}
|
||||
|
||||
func rejectPortalSSO(ctx *gin.Context, errorParam string) bool {
|
||||
if !loginaudience.Resolve(ctx.Query("next")).IsPortal() {
|
||||
return false
|
||||
}
|
||||
slog.Warn("rejected enterprise SSO started from the visitor portal",
|
||||
"provider", errorParam,
|
||||
"next", ctx.Query("next"),
|
||||
"clientIp", ctx.ClientIP(),
|
||||
)
|
||||
redirectPortalSSORejected(ctx, errorParam)
|
||||
return true
|
||||
}
|
||||
|
||||
func OIDCLogin(ctx *gin.Context) {
|
||||
if rejectPortalSSO(ctx, "oidcError") {
|
||||
return
|
||||
}
|
||||
loginURL, err := services.OIDCLoginService.BuildOIDCLoginURL(ctx.Query("next"))
|
||||
if err != nil {
|
||||
ctx.Redirect(http.StatusFound, "/dashboard/login?oidcError="+url.QueryEscape(loginErrorMessage(err.Error())))
|
||||
return
|
||||
}
|
||||
ctx.Redirect(http.StatusFound, loginURL)
|
||||
}
|
||||
|
||||
func OIDCCallback(ctx *gin.Context) {
|
||||
if oauthErr := ctx.Query("error"); oauthErr != "" {
|
||||
desc := ctx.Query("error_description")
|
||||
slog.Warn("oidc callback returned oauth error", "error", oauthErr, "description", desc)
|
||||
errMsg := oauthErr
|
||||
if desc != "" {
|
||||
errMsg += ": " + desc
|
||||
}
|
||||
ctx.Redirect(http.StatusFound, "/dashboard/login?oidcError="+url.QueryEscape(errMsg))
|
||||
return
|
||||
}
|
||||
|
||||
code := ctx.Query("code")
|
||||
state := ctx.Query("state")
|
||||
if strings.TrimSpace(code) == "" {
|
||||
slog.Warn("oidc callback missing code", "rawQuery", ctx.Request.URL.RawQuery)
|
||||
}
|
||||
|
||||
cfg := config.Current()
|
||||
ticket, next, err := services.OIDCLoginService.LoginByOIDC(
|
||||
ctx.Request.Context(),
|
||||
code,
|
||||
state,
|
||||
cfg.Auth,
|
||||
ctx.ClientIP(),
|
||||
ctx.GetHeader("User-Agent"),
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("oidc callback login failed", "error", err)
|
||||
ctx.Redirect(http.StatusFound, "/dashboard/login?oidcError="+url.QueryEscape(loginErrorMessage(err.Error())))
|
||||
return
|
||||
}
|
||||
ctx.Redirect(http.StatusFound, "/dashboard/login/oidc/callback?ticket="+url.QueryEscape(ticket)+"&next="+url.QueryEscape(next))
|
||||
}
|
||||
|
||||
func OIDCExchange(ctx *gin.Context) {
|
||||
req := request.OIDCExchangeRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
ret, err := services.OIDCLoginService.ExchangeOIDCLoginTicket(req.Ticket)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, ret)
|
||||
}
|
||||
|
||||
func Logout(ctx *gin.Context) {
|
||||
if err := services.AuthService.Logout(ctx.GetHeader("Authorization")); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func Profile(ctx *gin.Context) {
|
||||
ret, err := services.AuthService.CurrentProfile(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, ret)
|
||||
}
|
||||
|
||||
func UpdateProfile(ctx *gin.Context) {
|
||||
req := request.UpdateProfileRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
ret, err := services.AuthService.UpdateProfile(ctx, req)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, ret)
|
||||
}
|
||||
|
||||
func UploadProfileAvatar(ctx *gin.Context) {
|
||||
principal, err := services.AuthService.Authenticate(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
header, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0323"))
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(strings.ToLower(header.Header.Get("Content-Type")), "image/") {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0090"))
|
||||
return
|
||||
}
|
||||
|
||||
item, err := services.AssetService.UploadFile(header, "avatars", principal)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAsset(item))
|
||||
}
|
||||
|
||||
func loginErrorMessage(message string) string {
|
||||
if idx := strings.Index(message, ": "); idx >= 0 {
|
||||
message = message[idx+2:]
|
||||
}
|
||||
return message
|
||||
}
|
||||
65
internal/handlers/api/channel_handler.go
Normal file
65
internal/handlers/api/channel_handler.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ChannelAnyConfig(ctx *gin.Context) {
|
||||
channel := services.ChannelService.GetEnabledChannel(ctx)
|
||||
if channel == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
cfg, err := resolveWidgetConfig(channel.ChannelType, channel.ConfigJSON)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
ret := response.WidgetConfigResponse{
|
||||
ChannelID: channel.ChannelID,
|
||||
ChannelType: channel.ChannelType,
|
||||
Title: cfg.Title,
|
||||
Subtitle: cfg.Subtitle,
|
||||
ThemeColor: cfg.ThemeColor,
|
||||
Position: cfg.Position,
|
||||
Width: cfg.Width,
|
||||
PromptStarters: cfg.PromptStarters,
|
||||
}
|
||||
httpx.WriteJSON(ctx, ret)
|
||||
}
|
||||
|
||||
type webLikeWidgetConfig struct {
|
||||
Title string
|
||||
Subtitle string
|
||||
ThemeColor string
|
||||
Position string
|
||||
Width string
|
||||
PromptStarters []string
|
||||
}
|
||||
|
||||
func resolveWidgetConfig(channelType, rawConfig string) (*webLikeWidgetConfig, error) {
|
||||
switch channelType {
|
||||
case enums.ChannelTypeWeb:
|
||||
cfg, err := services.ChannelService.ParseWebChannelConfig(rawConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &webLikeWidgetConfig{
|
||||
Title: cfg.Title,
|
||||
Subtitle: cfg.Subtitle,
|
||||
ThemeColor: cfg.ThemeColor,
|
||||
Position: cfg.Position,
|
||||
Width: cfg.Width,
|
||||
PromptStarters: cfg.PromptStarters,
|
||||
}, nil
|
||||
default:
|
||||
return nil, errorsx.InvalidParamI18n("error.e0313")
|
||||
}
|
||||
}
|
||||
56
internal/handlers/api/channel_handler_test.go
Normal file
56
internal/handlers/api/channel_handler_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChannelConfigPromptStarters(t *testing.T) {
|
||||
rawConfig := `{
|
||||
"title": "Support Desk",
|
||||
"subtitle": "How can we help?",
|
||||
"themeColor": "#2563eb",
|
||||
"position": "right",
|
||||
"width": "380px",
|
||||
"promptStarters": [
|
||||
"How do I reset my password?",
|
||||
"What are your business hours?",
|
||||
"Can I talk to an agent?"
|
||||
]
|
||||
}`
|
||||
|
||||
cfg, err := resolveWidgetConfig(enums.ChannelTypeWeb, rawConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if cfg == nil {
|
||||
t.Fatalf("expected non-nil config")
|
||||
}
|
||||
|
||||
expectedStarters := []string{
|
||||
"How do I reset my password?",
|
||||
"What are your business hours?",
|
||||
"Can I talk to an agent?",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cfg.PromptStarters, expectedStarters) {
|
||||
t.Fatalf("expected prompt starters %v, got %v", expectedStarters, cfg.PromptStarters)
|
||||
}
|
||||
|
||||
resp := response.WidgetConfigResponse{
|
||||
ChannelID: "test-channel",
|
||||
ChannelType: enums.ChannelTypeWeb,
|
||||
Title: cfg.Title,
|
||||
Subtitle: cfg.Subtitle,
|
||||
ThemeColor: cfg.ThemeColor,
|
||||
Position: cfg.Position,
|
||||
Width: cfg.Width,
|
||||
PromptStarters: cfg.PromptStarters,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(resp.PromptStarters, expectedStarters) {
|
||||
t.Fatalf("expected response prompt starters %v, got %v", expectedStarters, resp.PromptStarters)
|
||||
}
|
||||
}
|
||||
176
internal/handlers/api/conversation_handler.go
Normal file
176
internal/handlers/api/conversation_handler.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func ConversationGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if services.ChannelService.GetEnabledChannel(ctx) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
external := httpx.GetExternalUser(ctx)
|
||||
if external == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150"))
|
||||
return
|
||||
}
|
||||
|
||||
item := services.ConversationService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0116"))
|
||||
return
|
||||
}
|
||||
if !services.ConversationService.IsCustomerConversationOwner(item, *external) {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0222"))
|
||||
return
|
||||
}
|
||||
|
||||
detail := response.ConversationDetailResponse{
|
||||
ConversationResponse: builders.BuildConversationWithLocale(item, i18nx.Locale(ctx)),
|
||||
Participants: builders.BuildParticipantResponses(id),
|
||||
}
|
||||
httpx.WriteJSON(ctx, detail)
|
||||
}
|
||||
|
||||
func ConversationPostCreate_or_match(ctx *gin.Context) {
|
||||
channel := services.ChannelService.GetEnabledChannel(ctx)
|
||||
if channel == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
external := httpx.GetExternalUser(ctx)
|
||||
if external == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150"))
|
||||
return
|
||||
}
|
||||
|
||||
item, err := services.ConversationService.Create(*external, channel.ID, channel.AIAgentID)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildConversationWithLocale(item, i18nx.Locale(ctx)))
|
||||
}
|
||||
|
||||
// ConversationPostCreate starts a brand new request instead of rejoining the
|
||||
// visitor's unfinished one, which is what the "new request" action needs.
|
||||
func ConversationPostCreate(ctx *gin.Context) {
|
||||
channel := services.ChannelService.GetEnabledChannel(ctx)
|
||||
if channel == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
external := httpx.GetExternalUser(ctx)
|
||||
if external == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150"))
|
||||
return
|
||||
}
|
||||
|
||||
// An absent body is valid: the subject is optional and the widget may post
|
||||
// nothing at all when the visitor skips the prompt.
|
||||
req := request.CreateCustomerConversationRequest{}
|
||||
if ctx.Request.ContentLength > 0 {
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
item, err := services.ConversationService.CreateNew(*external, channel.ID, channel.AIAgentID, req.Subject)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildConversationWithLocale(item, i18nx.Locale(ctx)))
|
||||
}
|
||||
|
||||
// ConversationAnyList returns the visitor's own requests so the widget can offer
|
||||
// a request list. Scoping comes from the external identity, never from a
|
||||
// client-supplied customer id.
|
||||
func ConversationAnyList(ctx *gin.Context) {
|
||||
if services.ChannelService.GetEnabledChannel(ctx) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
external := httpx.GetExternalUser(ctx)
|
||||
if external == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150"))
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
)
|
||||
list, paging, err := services.ConversationService.ListCustomerConversations(*external, cnd)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
locale := i18nx.Locale(ctx)
|
||||
results := make([]response.ConversationResponse, 0, len(list))
|
||||
for i := range list {
|
||||
results = append(results, builders.BuildConversationWithLocale(&list[i], locale))
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func ConversationPostClose(ctx *gin.Context) {
|
||||
if services.ChannelService.GetEnabledChannel(ctx) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
external := httpx.GetExternalUser(ctx)
|
||||
if external == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150"))
|
||||
return
|
||||
}
|
||||
|
||||
req := request.CloseConversationRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ConversationService.CloseCustomerConversation(req.ConversationID, *external); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ConversationPostSatisfaction(ctx *gin.Context) {
|
||||
if services.ChannelService.GetEnabledChannel(ctx) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
external := httpx.GetExternalUser(ctx)
|
||||
if external == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150"))
|
||||
return
|
||||
}
|
||||
|
||||
req := request.SubmitSatisfactionRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.ConversationSatisfactionService.SubmitSatisfaction(req.ConversationID, *external, req.Score, req.Feedback)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildSatisfactionResponse(item))
|
||||
}
|
||||
336
internal/handlers/api/conversation_handler_test.go
Normal file
336
internal/handlers/api/conversation_handler_test.go
Normal file
@@ -0,0 +1,336 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/config"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/openidentity"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
func setupConversationAPITestDB(t *testing.T) (*gorm.DB, *models.Channel, *models.AIAgent, openidentity.ExternalUser) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
|
||||
db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"), &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: "t_",
|
||||
SingularTable: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if sqlDB, err := db.DB(); err == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
})
|
||||
|
||||
if err := db.AutoMigrate(
|
||||
&models.Customer{},
|
||||
&models.CustomerIdentity{},
|
||||
&models.Channel{},
|
||||
&models.AIAgent{},
|
||||
&models.Conversation{},
|
||||
&models.ConversationAssignment{},
|
||||
&models.ConversationParticipant{},
|
||||
&models.ConversationEventLog{},
|
||||
&models.ConversationReadState{},
|
||||
&models.ConversationSatisfaction{},
|
||||
&models.Message{},
|
||||
&models.User{},
|
||||
); err != nil {
|
||||
t.Fatalf("auto migrate error = %v", err)
|
||||
}
|
||||
sqls.SetDB(db)
|
||||
|
||||
previousConfig := config.GetCurrent()
|
||||
config.SetCurrent(&config.Config{
|
||||
Conversation: config.ConversationConfig{CustomerMaxOpen: 5},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
config.SetCurrent(previousConfig)
|
||||
})
|
||||
|
||||
aiAgent := models.AIAgent{
|
||||
Name: "CSAT Bot",
|
||||
ServiceMode: enums.IMConversationServiceModeAIFirst,
|
||||
Status: enums.StatusOk,
|
||||
}
|
||||
if err := db.Create(&aiAgent).Error; err != nil {
|
||||
t.Fatalf("create ai agent error = %v", err)
|
||||
}
|
||||
|
||||
channel := models.Channel{
|
||||
ChannelID: "web-channel-csat",
|
||||
Name: "Web Channel",
|
||||
ChannelType: enums.ChannelTypeWeb,
|
||||
AIAgentID: aiAgent.ID,
|
||||
Status: enums.StatusOk,
|
||||
}
|
||||
if err := db.Create(&channel).Error; err != nil {
|
||||
t.Fatalf("create channel error = %v", err)
|
||||
}
|
||||
|
||||
external := openidentity.ExternalUser{
|
||||
ExternalSource: enums.ExternalSourceUser,
|
||||
ExternalID: "visitor_123",
|
||||
ExternalName: "Visitor 123",
|
||||
}
|
||||
|
||||
return db, &channel, &aiAgent, external
|
||||
}
|
||||
|
||||
func TestConversationSatisfactionAPI(t *testing.T) {
|
||||
_, channel, aiAgent, external := setupConversationAPITestDB(t)
|
||||
|
||||
// Create conversation and close it
|
||||
conv, err := services.ConversationService.CreateNew(external, channel.ID, aiAgent.ID, "Need help with service")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateNew error = %v", err)
|
||||
}
|
||||
if err := services.ConversationService.CloseCustomerConversation(conv.ID, external); err != nil {
|
||||
t.Fatalf("CloseCustomerConversation error = %v", err)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/conversation/satisfaction", func(c *gin.Context) {
|
||||
ConversationPostSatisfaction(c)
|
||||
})
|
||||
router.GET("/api/conversation/:id", func(c *gin.Context) {
|
||||
ConversationGetBy(c)
|
||||
})
|
||||
|
||||
t.Run("missing channel returns error", func(t *testing.T) {
|
||||
body, _ := json.Marshal(request.SubmitSatisfactionRequest{
|
||||
ConversationID: conv.ID,
|
||||
Score: 5,
|
||||
Feedback: "Great",
|
||||
})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/conversation/satisfaction", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
// no channel header
|
||||
httpx.SetExternalUser(&gin.Context{}, &external)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["success"] == true {
|
||||
t.Fatalf("expected failure without channel, got success: true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing external user returns error", func(t *testing.T) {
|
||||
body, _ := json.Marshal(request.SubmitSatisfactionRequest{
|
||||
ConversationID: conv.ID,
|
||||
Score: 5,
|
||||
Feedback: "Great",
|
||||
})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/conversation/satisfaction", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Channel-ID", channel.ChannelID)
|
||||
// no external user set in context
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["success"] == true {
|
||||
t.Fatalf("expected failure without external user, got success: true")
|
||||
}
|
||||
})
|
||||
|
||||
// Helper for authorized requests
|
||||
authRouter := gin.New()
|
||||
authRouter.Use(func(c *gin.Context) {
|
||||
httpx.SetExternalUser(c, &external)
|
||||
c.Next()
|
||||
})
|
||||
authRouter.POST("/api/conversation/satisfaction", ConversationPostSatisfaction)
|
||||
authRouter.GET("/api/conversation/:id", ConversationGetBy)
|
||||
|
||||
t.Run("rejects invalid score", func(t *testing.T) {
|
||||
invalidScores := []int{0, -1, 6, 10}
|
||||
for _, score := range invalidScores {
|
||||
body, _ := json.Marshal(request.SubmitSatisfactionRequest{
|
||||
ConversationID: conv.ID,
|
||||
Score: score,
|
||||
Feedback: "Invalid score test",
|
||||
})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/conversation/satisfaction", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Channel-ID", channel.ChannelID)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
authRouter.ServeHTTP(rec, req)
|
||||
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["success"] == true {
|
||||
t.Fatalf("expected failure for score %d, got success: true", score)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects unauthorized external user", func(t *testing.T) {
|
||||
attacker := openidentity.ExternalUser{
|
||||
ExternalSource: enums.ExternalSourceUser,
|
||||
ExternalID: "attacker_999",
|
||||
ExternalName: "Attacker",
|
||||
}
|
||||
attackerRouter := gin.New()
|
||||
attackerRouter.Use(func(c *gin.Context) {
|
||||
httpx.SetExternalUser(c, &attacker)
|
||||
c.Next()
|
||||
})
|
||||
attackerRouter.POST("/api/conversation/satisfaction", ConversationPostSatisfaction)
|
||||
|
||||
body, _ := json.Marshal(request.SubmitSatisfactionRequest{
|
||||
ConversationID: conv.ID,
|
||||
Score: 5,
|
||||
Feedback: "Hacked rating",
|
||||
})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/conversation/satisfaction", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Channel-ID", channel.ChannelID)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
attackerRouter.ServeHTTP(rec, req)
|
||||
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["success"] == true {
|
||||
t.Fatalf("expected forbidden failure for non-owner, got success: true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successful submission and response mapping", func(t *testing.T) {
|
||||
body, _ := json.Marshal(request.SubmitSatisfactionRequest{
|
||||
ConversationID: conv.ID,
|
||||
Score: 5,
|
||||
Feedback: "Outstanding support!",
|
||||
})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/conversation/satisfaction", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Channel-ID", channel.ChannelID)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
authRouter.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status code = %d, want 200", rec.Code)
|
||||
}
|
||||
|
||||
type JSONResult struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Ok bool `json:"success"`
|
||||
Data *response.ConversationSatisfactionResponse `json:"data"`
|
||||
}
|
||||
var result JSONResult
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
if !result.Ok {
|
||||
t.Fatalf("expected success: true, got code=%d msg=%s", result.Code, result.Msg)
|
||||
}
|
||||
if result.Data == nil {
|
||||
t.Fatalf("expected non-nil data")
|
||||
}
|
||||
if result.Data.ConversationID != conv.ID {
|
||||
t.Fatalf("ConversationID = %d, want %d", result.Data.ConversationID, conv.ID)
|
||||
}
|
||||
if result.Data.Score != 5 {
|
||||
t.Fatalf("Score = %d, want 5", result.Data.Score)
|
||||
}
|
||||
if result.Data.Feedback != "Outstanding support!" {
|
||||
t.Fatalf("Feedback = %q, want %q", result.Data.Feedback, "Outstanding support!")
|
||||
}
|
||||
if result.Data.ID == 0 {
|
||||
t.Fatalf("expected non-zero satisfaction ID")
|
||||
}
|
||||
if result.Data.CreatedAt == "" {
|
||||
t.Fatalf("expected non-empty CreatedAt")
|
||||
}
|
||||
|
||||
// Verify ConversationGetBy now returns satisfaction
|
||||
getReq, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/conversation/%d", conv.ID), nil)
|
||||
getReq.Header.Set("X-Channel-ID", channel.ChannelID)
|
||||
getRec := httptest.NewRecorder()
|
||||
authRouter.ServeHTTP(getRec, getReq)
|
||||
|
||||
type DetailResult struct {
|
||||
Ok bool `json:"success"`
|
||||
Data *response.ConversationDetailResponse `json:"data"`
|
||||
}
|
||||
var detailResult DetailResult
|
||||
if err := json.Unmarshal(getRec.Body.Bytes(), &detailResult); err != nil {
|
||||
t.Fatalf("failed to unmarshal detail response: %v", err)
|
||||
}
|
||||
if !detailResult.Ok || detailResult.Data == nil {
|
||||
t.Fatalf("expected success detail result")
|
||||
}
|
||||
if detailResult.Data.Satisfaction == nil {
|
||||
t.Fatalf("expected ConversationResponse to include satisfaction, got nil")
|
||||
}
|
||||
if detailResult.Data.Satisfaction.Score != 5 {
|
||||
t.Fatalf("satisfaction score = %d, want 5", detailResult.Data.Satisfaction.Score)
|
||||
}
|
||||
if detailResult.Data.Satisfaction.Feedback != "Outstanding support!" {
|
||||
t.Fatalf("satisfaction feedback = %q, want %q", detailResult.Data.Satisfaction.Feedback, "Outstanding support!")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("idempotent update submission", func(t *testing.T) {
|
||||
body, _ := json.Marshal(request.SubmitSatisfactionRequest{
|
||||
ConversationID: conv.ID,
|
||||
Score: 4,
|
||||
Feedback: "Updated to 4 stars",
|
||||
})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/conversation/satisfaction", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Channel-ID", channel.ChannelID)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
authRouter.ServeHTTP(rec, req)
|
||||
|
||||
var result struct {
|
||||
Ok bool `json:"success"`
|
||||
Data *response.ConversationSatisfactionResponse `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil {
|
||||
t.Fatalf("unmarshal error: %v", err)
|
||||
}
|
||||
if !result.Ok || result.Data == nil {
|
||||
t.Fatalf("expected success result")
|
||||
}
|
||||
if result.Data.Score != 4 {
|
||||
t.Fatalf("updated score = %d, want 4", result.Data.Score)
|
||||
}
|
||||
if result.Data.Feedback != "Updated to 4 stars" {
|
||||
t.Fatalf("updated feedback = %q, want %q", result.Data.Feedback, "Updated to 4 stars")
|
||||
}
|
||||
})
|
||||
}
|
||||
35
internal/handlers/api/customer_handler.go
Normal file
35
internal/handlers/api/customer_handler.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/config"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/openidentity"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CustomerPostSession_exchange(ctx *gin.Context) {
|
||||
channel := services.ChannelService.GetEnabledChannel(ctx)
|
||||
if channel == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0209"))
|
||||
return
|
||||
}
|
||||
identity := config.Current().Identity
|
||||
externalUser, err := openidentity.GetExternalUser(ctx, openidentity.Secrets{
|
||||
UserToken: services.ChannelService.GetUserTokenSecret(channel),
|
||||
IdentityHMAC: identity.Secret,
|
||||
IdentityMaxAge: identity.MaxAge(),
|
||||
ChannelID: channel.ChannelID,
|
||||
})
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := services.CustomerSessionService.Exchange(channel, *externalUser)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, resp)
|
||||
}
|
||||
15
internal/handlers/api/health_handler.go
Normal file
15
internal/handlers/api/health_handler.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type healthResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func Health(ctx *gin.Context) {
|
||||
httpx.WriteJSON(ctx, &healthResponse{Status: "ok"})
|
||||
}
|
||||
196
internal/handlers/api/message_handler.go
Normal file
196
internal/handlers/api/message_handler.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
func MessageAnyList(ctx *gin.Context) {
|
||||
if services.ChannelService.GetEnabledChannel(ctx) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
external := httpx.GetExternalUser(ctx)
|
||||
if external == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150"))
|
||||
return
|
||||
}
|
||||
|
||||
conversationID, _ := params.GetInt64(ctx, "conversationId")
|
||||
if conversationID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064"))
|
||||
return
|
||||
}
|
||||
conversation := services.ConversationService.Get(conversationID)
|
||||
if conversation == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0116"))
|
||||
return
|
||||
}
|
||||
if !services.ConversationService.IsCustomerConversationOwner(conversation, *external) {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0222"))
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
senderType, _ = params.Get(ctx, "senderType")
|
||||
messageType, _ = params.Get(ctx, "messageType")
|
||||
cursor, _ = params.GetInt64(ctx, "cursor")
|
||||
limit, _ = params.GetInt(ctx, "limit")
|
||||
)
|
||||
list, nextCursor, hasMore := services.MessageService.FindByConversationIDCursor(
|
||||
conversationID, cursor, limit, senderType, messageType,
|
||||
)
|
||||
results := builders.BuildMessagesWithLocale(list, i18nx.Locale(ctx))
|
||||
httpx.WriteJSON(ctx, httpx.CursorData(results, cast.ToString(nextCursor), hasMore))
|
||||
}
|
||||
|
||||
func MessagePostSend(ctx *gin.Context) {
|
||||
if services.ChannelService.GetEnabledChannel(ctx) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
external := httpx.GetExternalUser(ctx)
|
||||
if external == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150"))
|
||||
return
|
||||
}
|
||||
|
||||
req := request.SendConversationMessageRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item, err := services.MessageService.SendCustomerMessageWithRequestID(req.ConversationID, req.ClientMsgID, req.MessageType, req.Content, req.Payload, *external, httpx.GetRequestID(ctx))
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildMessageWithLocale(item, i18nx.Locale(ctx)))
|
||||
}
|
||||
|
||||
func MessagePostRead(ctx *gin.Context) {
|
||||
if services.ChannelService.GetEnabledChannel(ctx) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
external := httpx.GetExternalUser(ctx)
|
||||
if external == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150"))
|
||||
return
|
||||
}
|
||||
|
||||
req := request.ReadConversationRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ConversationService.MarkCustomerConversationReadToMessage(req.ConversationID, req.MessageID, external); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func MessagePostUpload_image(ctx *gin.Context) {
|
||||
if services.ChannelService.GetEnabledChannel(ctx) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
external := httpx.GetExternalUser(ctx)
|
||||
if external == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150"))
|
||||
return
|
||||
}
|
||||
|
||||
rawConv := strings.TrimSpace(params.FormValue(ctx, "conversationId"))
|
||||
if rawConv == "" {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064"))
|
||||
return
|
||||
}
|
||||
conversationID, err := strconv.ParseInt(rawConv, 10, 64)
|
||||
if err != nil || conversationID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064"))
|
||||
return
|
||||
}
|
||||
conversation := services.ConversationService.Get(conversationID)
|
||||
if conversation == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0116"))
|
||||
return
|
||||
}
|
||||
if !services.ConversationService.IsCustomerConversationOwner(conversation, *external) {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0222"))
|
||||
return
|
||||
}
|
||||
if _, err := services.MessageService.ValidateConversationSender(conversationID, enums.IMSenderTypeCustomer, nil, external); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
header, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0322"))
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(strings.ToLower(header.Header.Get("Content-Type")), "image/") {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0090"))
|
||||
return
|
||||
}
|
||||
|
||||
item, err := services.AssetService.UploadFile(header, "images", nil)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAsset(item))
|
||||
}
|
||||
|
||||
func MessagePostUpload_attachment(ctx *gin.Context) {
|
||||
if services.ChannelService.GetEnabledChannel(ctx) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0211"))
|
||||
return
|
||||
}
|
||||
external := httpx.GetExternalUser(ctx)
|
||||
if external == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0150"))
|
||||
return
|
||||
}
|
||||
|
||||
rawConv := strings.TrimSpace(params.FormValue(ctx, "conversationId"))
|
||||
if rawConv == "" {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064"))
|
||||
return
|
||||
}
|
||||
conversationID, err := strconv.ParseInt(rawConv, 10, 64)
|
||||
if err != nil || conversationID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064"))
|
||||
return
|
||||
}
|
||||
if _, err := services.MessageService.ValidateConversationSender(conversationID, enums.IMSenderTypeCustomer, nil, external); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
header, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0324"))
|
||||
return
|
||||
}
|
||||
item, err := services.AssetService.UploadFile(header, "attachments", nil)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAsset(item))
|
||||
}
|
||||
12
internal/handlers/api/support_config_handler.go
Normal file
12
internal/handlers/api/support_config_handler.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SupportConfigGetConfig(ctx *gin.Context) {
|
||||
httpx.WriteJSON(ctx, services.SystemConfigService.GetPublicSupportConfig())
|
||||
}
|
||||
356
internal/handlers/api/support_handler.go
Normal file
356
internal/handlers/api/support_handler.go
Normal file
@@ -0,0 +1,356 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/config"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/repositories"
|
||||
"agent-desk/internal/services"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"github.com/mlogclub/simple/web"
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SupportAuthPostRegister(ctx *gin.Context) {
|
||||
cfg := config.Current()
|
||||
if !cfg.Auth.IsPasswordLoginEnabled() {
|
||||
httpx.WriteJSON(ctx, errorsx.ForbiddenI18n("error.auth.passwordLoginDisabled"))
|
||||
return
|
||||
}
|
||||
|
||||
req := request.SupportCustomerRegisterRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
ret, err := services.SupportService.RegisterUser(req, cfg.Auth, ctx.ClientIP(), ctx.GetHeader("User-Agent"))
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, ret)
|
||||
}
|
||||
|
||||
func SupportGetMe(ctx *gin.Context) {
|
||||
principal, err := services.SupportService.RequireSupportUser(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
user := supportUser(principal.UserID)
|
||||
email := ""
|
||||
if user != nil && user.Email != nil {
|
||||
email = *user.Email
|
||||
}
|
||||
httpx.WriteJSON(ctx, response.SupportUserResponse{ID: principal.UserID, Name: supportPrincipalDisplayName(principal.UserID), Email: email, UserType: principal.UserType})
|
||||
}
|
||||
|
||||
func DocPageAnyList(ctx *gin.Context) {
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "parentId"},
|
||||
params.QueryFilter{ParamName: "title", Op: params.Like},
|
||||
).Eq("status", enums.DocPageStatusPublished).Asc("sort_no").Desc("id")
|
||||
if keyword := strings.TrimSpace(ctx.Query("keyword")); keyword != "" {
|
||||
pattern := "%" + keyword + "%"
|
||||
cnd.Where("(title LIKE ? OR summary LIKE ? OR content LIKE ? OR tags_json LIKE ? OR slug LIKE ?)", pattern, pattern, pattern, pattern, pattern)
|
||||
}
|
||||
list, paging := repositories.DocPageRepository.FindPageByCnd(sqls.DB(), cnd)
|
||||
results := buildDocPageList(list, false)
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func DocPageGetNavigation(ctx *gin.Context) {
|
||||
list := services.SupportService.FindPublicDocNavigation()
|
||||
httpx.WriteJSON(ctx, builders.BuildDocPageNavigationTree(list))
|
||||
}
|
||||
|
||||
func DocPageGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
item := repositories.DocPageRepository.Get(sqls.DB(), id)
|
||||
if item == nil || item.Status != enums.DocPageStatusPublished {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.notFound"))
|
||||
return
|
||||
}
|
||||
_ = repositories.DocPageRepository.UpdateColumn(sqls.DB(), item.ID, "view_count", gorm.Expr("view_count + ?", 1))
|
||||
httpx.WriteJSON(ctx, builders.BuildDocPage(item, true))
|
||||
}
|
||||
|
||||
func DocPagePostFeedback(ctx *gin.Context) {
|
||||
req := request.DocPageFeedbackRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.FeedbackDocPage(req))
|
||||
}
|
||||
|
||||
func CategoryAnyList(ctx *gin.Context) {
|
||||
list := repositories.CategoryRepository.Find(sqls.DB(), sqls.NewCnd().Eq("status", enums.StatusOk).Asc("sort_no").Desc("id"))
|
||||
httpx.WriteJSON(ctx, builders.BuildPostCategories(list))
|
||||
}
|
||||
|
||||
func PostAnyList(ctx *gin.Context) {
|
||||
cursor, _ := params.GetInt64(ctx, "cursor")
|
||||
limit, _ := params.GetInt(ctx, "limit")
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 20
|
||||
}
|
||||
cnd := params.NewSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "categoryId"},
|
||||
params.QueryFilter{ParamName: "userId"},
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "title", Op: params.Like},
|
||||
).Where("status NOT IN ?", []enums.PostStatus{enums.PostStatusHidden, enums.PostStatusDeleted}).Desc("id").Limit(limit + 1)
|
||||
if cursor > 0 {
|
||||
cnd.Lt("id", cursor)
|
||||
}
|
||||
list := repositories.PostRepository.Find(sqls.DB(), cnd)
|
||||
hasMore := len(list) > limit
|
||||
if hasMore {
|
||||
list = list[:limit]
|
||||
}
|
||||
nextCursor := ""
|
||||
if hasMore && len(list) > 0 {
|
||||
nextCursor = cast.ToString(list[len(list)-1].ID)
|
||||
}
|
||||
httpx.WriteJSON(ctx, httpx.CursorData(buildPostList(list), nextCursor, hasMore))
|
||||
}
|
||||
|
||||
func PostGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
post := repositories.PostRepository.Get(sqls.DB(), id)
|
||||
if post == nil || post.Status == enums.PostStatusHidden || post.Status == enums.PostStatusDeleted {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.notFound"))
|
||||
return
|
||||
}
|
||||
_ = repositories.PostRepository.UpdateColumn(sqls.DB(), post.ID, "view_count", gorm.Expr("view_count + ?", 1))
|
||||
comments, err := services.SupportService.ListPostComments(id, 0, "default", 1, 20)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, response.PostDetailResponse{Post: *builders.BuildPost(post, supportCategoryName(post.CategoryID), supportUser(post.UserID)), Comments: buildCommentListWithReplies(comments.Comments, comments.Replies)})
|
||||
}
|
||||
|
||||
func PostPostCreate(ctx *gin.Context) {
|
||||
principal, err := services.SupportService.RequireSupportUser(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreatePostRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.SupportService.CreatePost(req, principal)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildPost(item, supportCategoryName(item.CategoryID), supportUser(principal.UserID)))
|
||||
}
|
||||
|
||||
func PostPostUpdate(ctx *gin.Context) {
|
||||
principal, err := services.SupportService.RequireSupportUser(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdatePostRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.UpdatePost(req, principal))
|
||||
}
|
||||
|
||||
func PostPostAcceptComment(ctx *gin.Context) {
|
||||
principal, err := services.SupportService.RequireSupportUser(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.AcceptCommentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.AcceptComment(req, principal, nil))
|
||||
}
|
||||
|
||||
func CommentPostCreate(ctx *gin.Context) {
|
||||
principal, err := services.SupportService.RequireSupportUser(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateCommentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.SupportService.CreateCustomerComment(req, principal)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildComment(item, supportPrincipalDisplayName(principal.UserID)))
|
||||
}
|
||||
|
||||
func CommentAnyList(ctx *gin.Context) {
|
||||
postID, _ := params.GetInt64(ctx, "postId")
|
||||
parentID, _ := params.GetInt64(ctx, "parentId")
|
||||
page, _ := params.GetInt(ctx, "page")
|
||||
limit, _ := params.GetInt(ctx, "limit")
|
||||
sort, _ := params.Get(ctx, "sort")
|
||||
result, err := services.SupportService.ListPostComments(postID, parentID, sort, page, limit)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: buildCommentListWithReplies(result.Comments, result.Replies), Page: result.Paging})
|
||||
}
|
||||
|
||||
func CommentPostUpdate(ctx *gin.Context) {
|
||||
principal, err := services.SupportService.RequireSupportUser(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateCommentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.UpdateComment(req, principal))
|
||||
}
|
||||
|
||||
func CommentPostDelete(ctx *gin.Context) {
|
||||
principal, err := services.SupportService.RequireSupportUser(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.IDRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.DeleteComment(req.ID, principal))
|
||||
}
|
||||
|
||||
func CommentPostReport(ctx *gin.Context) {
|
||||
principal, err := services.SupportService.RequireSupportUser(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.ReportCommentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.ReportComment(req, principal))
|
||||
}
|
||||
|
||||
func ReactionPostToggle(ctx *gin.Context) {
|
||||
principal, err := services.SupportService.RequireSupportUser(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.ReactionRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.ToggleReaction(req.TargetType, req.TargetID, req.ReactionType, principal))
|
||||
}
|
||||
|
||||
func buildDocPageList(list []models.DocPage, includeContent bool) []response.DocPageResponse {
|
||||
results := make([]response.DocPageResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
if resp := builders.BuildDocPage(&item, includeContent); resp != nil {
|
||||
results = append(results, *resp)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func buildPostList(list []models.Post) []response.PostResponse {
|
||||
results := make([]response.PostResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
if resp := builders.BuildPost(&item, supportCategoryName(item.CategoryID), supportUser(item.UserID)); resp != nil {
|
||||
results = append(results, *resp)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func buildCommentList(list []models.Comment) []response.CommentResponse {
|
||||
return buildCommentListWithReplies(list, nil)
|
||||
}
|
||||
|
||||
func buildCommentListWithReplies(list []models.Comment, replies map[int64][]models.Comment) []response.CommentResponse {
|
||||
results := make([]response.CommentResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
if resp := builders.BuildComment(&item, supportCommentAuthorName(item)); resp != nil {
|
||||
if len(replies[item.ID]) > 0 {
|
||||
resp.Replies = buildCommentListWithReplies(replies[item.ID], nil)
|
||||
}
|
||||
results = append(results, *resp)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func supportCategoryName(id int64) string {
|
||||
item := repositories.CategoryRepository.Get(sqls.DB(), id)
|
||||
if item == nil {
|
||||
return ""
|
||||
}
|
||||
return item.Name
|
||||
}
|
||||
|
||||
func supportUser(id int64) *models.User {
|
||||
return repositories.UserRepository.Get(sqls.DB(), id)
|
||||
}
|
||||
|
||||
func supportPrincipalDisplayName(id int64) string {
|
||||
user := supportUser(id)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
if user.Nickname != "" {
|
||||
return user.Nickname
|
||||
}
|
||||
return user.Username
|
||||
}
|
||||
|
||||
func supportCommentAuthorName(item models.Comment) string {
|
||||
user := repositories.UserRepository.Get(sqls.DB(), item.AuthorID)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
if user.Nickname != "" {
|
||||
return user.Nickname
|
||||
}
|
||||
return user.Username
|
||||
}
|
||||
55
internal/handlers/api/webhook_handler.go
Normal file
55
internal/handlers/api/webhook_handler.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/services"
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func OrgSyncWebhook(ctx *gin.Context) {
|
||||
bodyBytes, err := io.ReadAll(ctx.Request.Body)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "failed to read body"})
|
||||
return
|
||||
}
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
signature := firstHeader(ctx, "X-Webhook-Signature", "X-Org-Signature", "X-DOS-Signature", "X-Hub-Signature-256")
|
||||
if !services.WebhookSyncService.VerifySignature(bodyBytes, signature) {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"})
|
||||
return
|
||||
}
|
||||
|
||||
req := request.OrgSyncWebhookRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.WebhookSyncService.HandleOrgSync(req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpx.WriteJSON(ctx, gin.H{"success": true})
|
||||
}
|
||||
|
||||
func DOSOrgSyncWebhook(ctx *gin.Context) {
|
||||
OrgSyncWebhook(ctx)
|
||||
}
|
||||
|
||||
func firstHeader(ctx *gin.Context, keys ...string) string {
|
||||
for _, k := range keys {
|
||||
if val := strings.TrimSpace(ctx.GetHeader(k)); val != "" {
|
||||
return val
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
116
internal/handlers/dashboard/agent_handler.go
Normal file
116
internal/handlers/dashboard/agent_handler.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func AgentAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list, paging := services.AgentProfileService.FindPageByCnd(params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "userId"},
|
||||
params.QueryFilter{ParamName: "teamId"},
|
||||
params.QueryFilter{ParamName: "serviceStatus"},
|
||||
params.QueryFilter{ParamName: "agentCode", Op: params.Like},
|
||||
params.QueryFilter{ParamName: "displayName", Op: params.Like},
|
||||
).Desc("id"))
|
||||
results := builders.BuildAgentProfileList(list)
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func AgentGetList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list := services.AgentProfileService.Find(params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "userId"},
|
||||
params.QueryFilter{ParamName: "teamId"},
|
||||
params.QueryFilter{ParamName: "serviceStatus"},
|
||||
params.QueryFilter{ParamName: "agentCode", Op: params.Like},
|
||||
).Desc("id"))
|
||||
|
||||
httpx.WriteJSON(ctx, builders.BuildAgentProfileList(list))
|
||||
}
|
||||
|
||||
func AgentGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item := services.AgentProfileService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0164"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAgentProfileResponse(item))
|
||||
}
|
||||
|
||||
func AgentPostCreate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateAgentProfileRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.AgentProfileService.CreateAgentProfile(req, user)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAgentProfileResponse(item))
|
||||
}
|
||||
|
||||
func AgentPostUpdate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateAgentProfileRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AgentProfileService.UpdateAgentProfile(req, user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AgentPostDelete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteAgentProfileRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AgentProfileService.DeleteAgentProfile(req.ID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
93
internal/handlers/dashboard/agent_run_handler.go
Normal file
93
internal/handlers/dashboard/agent_run_handler.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func AgentRunAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
queryParams := params.NewQueryParams(ctx)
|
||||
queryParams.Cnd = *params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "conversationId"},
|
||||
params.QueryFilter{ParamName: "aiAgentId"},
|
||||
params.QueryFilter{ParamName: "agentRevisionId"},
|
||||
params.QueryFilter{ParamName: "sourceMessageId"},
|
||||
params.QueryFilter{ParamName: "workflowRunId"},
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
).Desc("id")
|
||||
list, paging := services.AgentRunService.FindPageByParams(queryParams)
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: builders.BuildAgentRunList(list), Page: paging})
|
||||
}
|
||||
|
||||
func AgentRunGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
run, steps, toolCalls := services.AgentRunService.GetDetail(id)
|
||||
if run == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0002"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAgentRunDetail(run, steps, toolCalls, services.AgentRunService.GetQualityFeedback(run.ID)))
|
||||
}
|
||||
|
||||
func AgentRunPostSave_quality_feedback(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.SaveAgentRunQualityFeedbackRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AgentRunService.SaveQualityFeedback(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AgentRunAnyMetrics(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
aiAgentID, _ := params.GetInt64(ctx, "aiAgentId")
|
||||
httpx.WriteJSON(ctx, services.AgentRunService.GetMetrics(aiAgentID))
|
||||
}
|
||||
|
||||
func AgentRunPostEvaluate(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.RunAgentEvaluationRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
result, err := services.AgentEvaluationService.Run(ctx, req)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, result)
|
||||
}
|
||||
138
internal/handlers/dashboard/agent_team_handler.go
Normal file
138
internal/handlers/dashboard/agent_team_handler.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
func AgentTeamAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
cnd := params.NewSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "leaderUserId"},
|
||||
params.QueryFilter{ParamName: "name", Op: params.Like},
|
||||
).Desc("id")
|
||||
if _, ok := params.Get(ctx, "status"); !ok {
|
||||
cnd.Where("status <> ?", enums.StatusDeleted)
|
||||
}
|
||||
list := services.AgentTeamService.Find(cnd)
|
||||
results := make([]response.AgentTeamResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, buildAgentTeamResponse(&item))
|
||||
}
|
||||
httpx.WriteJSON(ctx, results)
|
||||
}
|
||||
|
||||
func AgentTeamGetList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list := services.AgentTeamService.Find(sqls.NewCnd().Eq("status", enums.StatusOk))
|
||||
results := make([]response.AgentTeamResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, buildAgentTeamResponse(&item))
|
||||
}
|
||||
httpx.WriteJSON(ctx, results)
|
||||
}
|
||||
|
||||
func AgentTeamGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item := services.AgentTeamService.Get(id)
|
||||
if item == nil || item.Status == enums.StatusDeleted {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0169"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, buildAgentTeamResponse(item))
|
||||
}
|
||||
|
||||
func AgentTeamPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateAgentTeamRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.AgentTeamService.CreateAgentTeam(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, buildAgentTeamResponse(item))
|
||||
}
|
||||
|
||||
func AgentTeamPostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateAgentTeamRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AgentTeamService.UpdateAgentTeam(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AgentTeamPostDelete(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamDelete)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteAgentTeamRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AgentTeamService.DeleteAgentTeam(req.ID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func buildAgentTeamResponse(item *models.AgentTeam) response.AgentTeamResponse {
|
||||
ret := response.AgentTeamResponse{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
LeaderUserID: item.LeaderUserID,
|
||||
Status: item.Status,
|
||||
Description: item.Description,
|
||||
Remark: item.Remark,
|
||||
}
|
||||
if user := services.UserService.Get(item.LeaderUserID); user != nil {
|
||||
ret.LeaderUsername = user.Username
|
||||
ret.LeaderNickname = user.Nickname
|
||||
}
|
||||
return ret
|
||||
}
|
||||
182
internal/handlers/dashboard/agent_team_schedule_handler.go
Normal file
182
internal/handlers/dashboard/agent_team_schedule_handler.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func AgentTeamScheduleAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamScheduleView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "teamId"},
|
||||
).Desc("start_at").Desc("id")
|
||||
list, paging := services.AgentTeamScheduleService.FindPageByCnd(cnd)
|
||||
results := make([]response.AgentTeamScheduleResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, buildAgentTeamScheduleResponse(&item))
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func AgentTeamScheduleAnyCalendar(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamScheduleView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
startAt, _ := params.Get(ctx, "startAt")
|
||||
endAt, _ := params.Get(ctx, "endAt")
|
||||
teamID, _ := params.GetInt64(ctx, "teamId")
|
||||
list, err := services.AgentTeamScheduleService.FindCalendarSchedules(request.AgentTeamScheduleCalendarRequest{
|
||||
StartAt: startAt,
|
||||
EndAt: endAt,
|
||||
TeamID: teamID,
|
||||
})
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
results := make([]response.AgentTeamScheduleResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, buildAgentTeamScheduleResponse(&item))
|
||||
}
|
||||
httpx.WriteJSON(ctx, results)
|
||||
}
|
||||
|
||||
func AgentTeamSchedulePostBatch_preview(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamScheduleBatchGenerate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.AgentTeamScheduleBatchRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req.Locale = i18nx.Locale(ctx)
|
||||
ret, err := services.AgentTeamScheduleService.BatchPreview(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAgentTeamScheduleBatchPreviewResponse(ret))
|
||||
}
|
||||
|
||||
func AgentTeamSchedulePostBatch_generate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamScheduleBatchGenerate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.AgentTeamScheduleBatchRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req.Locale = i18nx.Locale(ctx)
|
||||
ret, err := services.AgentTeamScheduleService.BatchGenerate(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAgentTeamScheduleBatchGenerateResponse(ret))
|
||||
}
|
||||
|
||||
func AgentTeamScheduleGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamScheduleView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item := services.AgentTeamScheduleService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0172"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, buildAgentTeamScheduleResponse(item))
|
||||
}
|
||||
|
||||
func AgentTeamSchedulePostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamScheduleCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateAgentTeamScheduleRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.AgentTeamScheduleService.CreateAgentTeamSchedule(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, buildAgentTeamScheduleResponse(item))
|
||||
}
|
||||
|
||||
func AgentTeamSchedulePostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamScheduleUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateAgentTeamScheduleRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AgentTeamScheduleService.UpdateAgentTeamSchedule(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AgentTeamSchedulePostDelete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAgentTeamScheduleDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteAgentTeamScheduleRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AgentTeamScheduleService.DeleteAgentTeamSchedule(req.ID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func buildAgentTeamScheduleResponse(item *models.AgentTeamSchedule) response.AgentTeamScheduleResponse {
|
||||
ret := response.AgentTeamScheduleResponse{
|
||||
ID: item.ID,
|
||||
TeamID: item.TeamID,
|
||||
StartAt: item.StartAt.Format("2006-01-02 15:04:05"),
|
||||
EndAt: item.EndAt.Format("2006-01-02 15:04:05"),
|
||||
Remark: item.Remark,
|
||||
}
|
||||
if team := services.AgentTeamService.Get(item.TeamID); team != nil {
|
||||
ret.TeamName = team.Name
|
||||
}
|
||||
return ret
|
||||
}
|
||||
354
internal/handlers/dashboard/ai_agent_handler.go
Normal file
354
internal/handlers/dashboard/ai_agent_handler.go
Normal file
@@ -0,0 +1,354 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/pkg/toolx"
|
||||
"agent-desk/internal/pkg/utils"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func AIAgentAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "name", Op: params.Like},
|
||||
params.QueryFilter{ParamName: "code", Op: params.Like},
|
||||
).Asc("sort_no").Desc("id")
|
||||
list, paging := services.AIAgentService.FindPageByCnd(cnd)
|
||||
results := make([]response.AIAgentResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, buildAIAgentResponseWithLocale(&item, i18nx.Locale(ctx)))
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func AIAgentGetList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list := services.AIAgentService.Find(sqls.NewCnd().Where("status = ?", enums.StatusOk).Desc("sort_no").Desc("id"))
|
||||
results := make([]response.AIAgentResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, buildAIAgentResponseWithLocale(&item, i18nx.Locale(ctx)))
|
||||
}
|
||||
httpx.WriteJSON(ctx, results)
|
||||
}
|
||||
|
||||
func AIAgentGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item := services.AIAgentService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0002"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, buildAIAgentResponseWithLocale(item, i18nx.Locale(ctx)))
|
||||
}
|
||||
|
||||
func AIAgentPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateAIAgentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.AIAgentService.CreateAIAgent(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, buildAIAgentResponseWithLocale(item, i18nx.Locale(ctx)))
|
||||
}
|
||||
|
||||
func AIAgentPostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateAIAgentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIAgentService.UpdateAIAgent(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIAgentPostDelete(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentDelete)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteAIAgentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIAgentService.DeleteAIAgent(req.ID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIAgentPostPublish(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.PublishAIAgentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
_, err = services.AIAgentService.PublishAIAgent(req.ID, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIAgentAnyRevisionList(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if services.AIAgentService.Get(id) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0002"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAgentRevisionList(services.AgentRevisionService.FindByAgentID(id)))
|
||||
}
|
||||
|
||||
func AIAgentPostRollback(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.RollbackAIAgentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIAgentService.RollbackAIAgent(req.ID, req.RevisionID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIAgentPostRollback_rollout(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.RollbackAIAgentRolloutRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIAgentService.RollbackAIAgentRollout(req.ID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIAgentPostUpdate_sort(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
var ids []int64
|
||||
if err := params.ReadJSON(ctx, &ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIAgentService.UpdateSort(ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIAgentPostUpdate_status(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateAIAgentStatusRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIAgentService.UpdateStatus(req.ID, req.Status, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func buildAIAgentResponse(item *models.AIAgent) response.AIAgentResponse {
|
||||
return buildAIAgentResponseWithLocale(item, i18nx.DefaultLocale)
|
||||
}
|
||||
|
||||
func buildAIAgentResponseWithLocale(item *models.AIAgent, locale string) response.AIAgentResponse {
|
||||
ret := response.AIAgentResponse{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Description: item.Description,
|
||||
Status: item.Status,
|
||||
StatusName: enums.GetStatusLabel(item.Status),
|
||||
AIConfigID: item.AIConfigID,
|
||||
MaxSteps: item.MaxSteps,
|
||||
ContextWindow: item.ContextWindow,
|
||||
ToolPolicy: item.ToolPolicy,
|
||||
KnowledgePolicy: item.KnowledgePolicy,
|
||||
ServiceMode: item.ServiceMode,
|
||||
ServiceModeName: enums.GetIMConversationServiceModeLabel(item.ServiceMode),
|
||||
SystemPrompt: item.SystemPrompt,
|
||||
WelcomeMessage: item.WelcomeMessage,
|
||||
ReplyTimeoutSeconds: item.ReplyTimeoutSeconds,
|
||||
RolloutPercent: item.RolloutPercent,
|
||||
PreviousRolloutPercent: item.PreviousRolloutPercent,
|
||||
HandoffMode: item.HandoffMode,
|
||||
HandoffModeName: enums.GetAIAgentHandoffModeLabel(item.HandoffMode),
|
||||
FallbackMode: item.FallbackMode,
|
||||
FallbackModeName: enums.GetAIAgentFallbackModeLabel(item.FallbackMode),
|
||||
FallbackMessage: item.FallbackMessage,
|
||||
KnowledgeBaseIDs: utils.SplitInt64s(item.KnowledgeIDs),
|
||||
SkillIDs: utils.SplitInt64s(item.SkillIDs),
|
||||
Skills: make([]response.AIAgentSkillResponse, 0),
|
||||
Teams: make([]response.AIAgentTeamResponse, 0),
|
||||
MCPTools: make([]response.AIAgentMCPToolResponse, 0),
|
||||
WorkflowBindings: make([]response.AIAgentWorkflowBindingResponse, 0),
|
||||
PublishedRevisionID: item.PublishedRevisionID,
|
||||
SortNo: item.SortNo,
|
||||
AutoHandoffOnFailure: item.AutoHandoffOnFailure,
|
||||
HandoffMessage: item.HandoffMessage,
|
||||
CreatedAt: item.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: item.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
CreateUserName: item.CreateUserName,
|
||||
UpdateUserName: item.UpdateUserName,
|
||||
}
|
||||
if aiConfig := services.AIConfigService.Get(item.AIConfigID); aiConfig != nil {
|
||||
ret.AIConfigName = aiConfig.Name
|
||||
}
|
||||
for _, id := range utils.SplitInt64s(item.TeamIDs) {
|
||||
if team := services.AgentTeamService.Get(id); team != nil {
|
||||
ret.Teams = append(ret.Teams, response.AIAgentTeamResponse{
|
||||
ID: team.ID,
|
||||
Name: team.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, id := range ret.SkillIDs {
|
||||
if skill := services.SkillDefinitionService.Get(id); skill != nil {
|
||||
ret.Skills = append(ret.Skills, response.AIAgentSkillResponse{
|
||||
ID: skill.ID,
|
||||
Name: skill.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
if raw := strings.TrimSpace(item.AllowedMCPTools); raw != "" {
|
||||
var mcpTools []request.AIAgentMCPToolRequest
|
||||
if err := json.Unmarshal([]byte(raw), &mcpTools); err == nil {
|
||||
for _, tool := range mcpTools {
|
||||
toolCode := strings.TrimSpace(tool.ToolCode)
|
||||
if toolCode == "" {
|
||||
toolCode = toolx.BuildMCPToolCode(tool.ServerCode, tool.ToolName)
|
||||
}
|
||||
toolCode = toolx.NormalizeToolCodeAlias(toolCode)
|
||||
if toolx.ResolveToolSourceType(toolCode) != enums.ToolSourceTypeMCP {
|
||||
continue
|
||||
}
|
||||
serverCode := strings.TrimSpace(tool.ServerCode)
|
||||
toolName := strings.TrimSpace(tool.ToolName)
|
||||
if registeredServerCode, registeredToolName, ok := toolx.GetRegisteredToolIdentity(toolCode); ok {
|
||||
serverCode = registeredServerCode
|
||||
toolName = registeredToolName
|
||||
} else if parsedServerCode, parsedToolName := toolx.SplitMCPToolCode(toolCode); parsedServerCode != "" && parsedToolName != "" {
|
||||
serverCode = parsedServerCode
|
||||
toolName = parsedToolName
|
||||
}
|
||||
title := strings.TrimSpace(tool.Title)
|
||||
if title == "" {
|
||||
if registeredTitle := toolx.GetRegisteredToolTitleLocale(toolCode, locale); registeredTitle != "" {
|
||||
title = registeredTitle
|
||||
}
|
||||
}
|
||||
description := strings.TrimSpace(tool.Description)
|
||||
if description == "" {
|
||||
if registeredDescription := toolx.GetRegisteredToolDescriptionLocale(toolCode, locale); registeredDescription != "" {
|
||||
description = registeredDescription
|
||||
}
|
||||
}
|
||||
ret.MCPTools = append(ret.MCPTools, response.AIAgentMCPToolResponse{
|
||||
ToolCode: toolCode,
|
||||
ServerCode: serverCode,
|
||||
ToolName: toolName,
|
||||
Title: title,
|
||||
Description: description,
|
||||
RiskLevel: tool.RiskLevel,
|
||||
RequireConfirmation: tool.RequireConfirmation,
|
||||
Arguments: tool.Arguments,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, binding := range services.AIAgentService.ListWorkflowBindings(item.ID) {
|
||||
if binding.Workflow == nil || binding.Version == nil {
|
||||
continue
|
||||
}
|
||||
ret.WorkflowBindings = append(ret.WorkflowBindings, response.AIAgentWorkflowBindingResponse{
|
||||
ID: binding.Binding.ID, WorkflowID: binding.Binding.WorkflowID, WorkflowVersionID: binding.Binding.WorkflowVersionID,
|
||||
WorkflowName: binding.Workflow.Name, WorkflowVersion: binding.Version.Version, ToolName: binding.Binding.ToolName,
|
||||
TriggerInstruction: binding.Binding.TriggerInstruction, Priority: binding.Binding.Priority, Enabled: binding.Binding.Enabled,
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
51
internal/handlers/dashboard/ai_agent_handler_test.go
Normal file
51
internal/handlers/dashboard/ai_agent_handler_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestBuildAIAgentResponseExposesPublishedRevision(t *testing.T) {
|
||||
setupAIAgentHandlerTestDB(t)
|
||||
|
||||
published := buildAIAgentResponse(&models.AIAgent{PublishedRevisionID: 12})
|
||||
if published.PublishedRevisionID != 12 {
|
||||
t.Fatalf("published revision = %d, want 12", published.PublishedRevisionID)
|
||||
}
|
||||
|
||||
rollout := buildAIAgentResponse(&models.AIAgent{RolloutPercent: 20, PreviousRolloutPercent: 100})
|
||||
if rollout.RolloutPercent != 20 || rollout.PreviousRolloutPercent != 100 {
|
||||
t.Fatalf("unexpected rollout response: %#v", rollout)
|
||||
}
|
||||
}
|
||||
|
||||
func setupAIAgentHandlerTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite db: %v", err)
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatalf("get sqlite db: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
t.Fatalf("close sqlite db: %v", err)
|
||||
}
|
||||
})
|
||||
if err := db.AutoMigrate(
|
||||
&models.AIConfig{},
|
||||
&models.AgentTeam{},
|
||||
&models.KnowledgeBase{},
|
||||
&models.SkillDefinition{},
|
||||
); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
}
|
||||
sqls.SetDB(db)
|
||||
}
|
||||
164
internal/handlers/dashboard/ai_config_handler.go
Normal file
164
internal/handlers/dashboard/ai_config_handler.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func AIConfigAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIConfigView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list, paging := services.AIConfigService.FindPageByCnd(params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "provider"},
|
||||
params.QueryFilter{ParamName: "modelType"},
|
||||
params.QueryFilter{ParamName: "name", Op: params.Like},
|
||||
params.QueryFilter{ParamName: "modelName", Op: params.Like},
|
||||
).Asc("sort_no").Desc("id"))
|
||||
results := make([]response.AIConfigResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, response.BuildAIConfigResponse(&item))
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func AIConfigAnyList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIConfigView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
list := services.AIConfigService.Find(params.NewSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "modelType"},
|
||||
).Eq("status", enums.StatusOk).Desc("sort_no").Desc("id"))
|
||||
|
||||
results := make([]response.AIConfigResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, response.BuildAIConfigResponse(&item))
|
||||
}
|
||||
httpx.WriteJSON(ctx, results)
|
||||
}
|
||||
|
||||
func AIConfigGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIConfigView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := services.AIConfigService.Get(id)
|
||||
if item == nil || item.Status == enums.StatusDeleted {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0012"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, response.BuildAIConfigResponse(item))
|
||||
}
|
||||
|
||||
func AIConfigPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIConfigCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.CreateAIConfigRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.AIConfigService.CreateAIConfig(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, response.BuildAIConfigResponse(item))
|
||||
}
|
||||
|
||||
func AIConfigPostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIConfigUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateAIConfigRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIConfigService.UpdateAIConfig(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIConfigPostDelete(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIConfigDelete)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.DeleteAIConfigRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIConfigService.DeleteAIConfig(req.ID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIConfigPostUpdate_status(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIConfigUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateAIConfigStatusRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIConfigService.UpdateStatus(req.ID, req.Status, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIConfigPostUpdateSort(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIConfigUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
var ids []int64
|
||||
if err := params.ReadJSON(ctx, &ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIConfigService.UpdateSort(ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
274
internal/handlers/dashboard/ai_workflow_handler.go
Normal file
274
internal/handlers/dashboard/ai_workflow_handler.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func AIWorkflowAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "name", Op: params.Like},
|
||||
).NotEq("status", enums.StatusDeleted).Desc("id")
|
||||
list, paging := services.AIWorkflowService.FindPageByCnd(cnd)
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: builders.BuildAIWorkflowList(list), Page: paging})
|
||||
}
|
||||
|
||||
func AIWorkflowGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item := services.AIWorkflowService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0002"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAIWorkflow(item))
|
||||
}
|
||||
|
||||
func AIWorkflowPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateAIWorkflowRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.AIWorkflowService.CreateWorkflow(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAIWorkflow(item))
|
||||
}
|
||||
|
||||
func AIWorkflowPostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateAIWorkflowRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIWorkflowService.UpdateWorkflow(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIWorkflowPostDelete(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentDelete)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteAIWorkflowRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIWorkflowService.DeleteWorkflow(req.ID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIWorkflowPostRestoreVersion(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.RestoreAIWorkflowVersionRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AIWorkflowService.RestoreVersion(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func AIWorkflowGetUsage(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
items := services.AIWorkflowService.ListUsage(id)
|
||||
ret := make([]response.AIWorkflowUsageResponse, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.Agent == nil || item.Version == nil {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, response.AIWorkflowUsageResponse{AIAgentID: item.Agent.ID, AIAgentName: item.Agent.Name, WorkflowVersionID: item.Binding.WorkflowVersionID, WorkflowVersion: item.Version.Version, Enabled: item.Binding.Enabled})
|
||||
}
|
||||
httpx.WriteJSON(ctx, ret)
|
||||
}
|
||||
|
||||
func AIWorkflowGetNodeSpecList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAIWorkflowNodeSpecs(services.AIWorkflowService.ListNodeSpecs()))
|
||||
}
|
||||
|
||||
func AIWorkflowGetDefaultDefinition(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.AIWorkflowService.DefaultAgentWorkflowDefinition())
|
||||
}
|
||||
|
||||
func AIWorkflowGetTemplateList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAIWorkflowTemplates(services.AIWorkflowService.ListWorkflowTemplates()))
|
||||
}
|
||||
|
||||
func AIWorkflowPostValidate(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.ValidateAIWorkflowRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
result := services.AIWorkflowService.ValidateDefinition(req.Definition)
|
||||
httpx.WriteJSON(ctx, response.AIWorkflowValidationResponse{
|
||||
Valid: result.Valid,
|
||||
Errors: result.Errors,
|
||||
})
|
||||
}
|
||||
|
||||
func AIWorkflowPostPublish(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.PublishAIWorkflowRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.AIWorkflowService.PublishWorkflow(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAIWorkflowVersion(item))
|
||||
}
|
||||
|
||||
func AIWorkflowAnyVersionList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
queryParams := params.NewQueryParams(ctx)
|
||||
cnd := params.NewPagedSqlCnd(ctx, params.QueryFilter{ParamName: "workflowId"}).Desc("version").Desc("id")
|
||||
queryParams.Cnd = *cnd
|
||||
list, paging := services.AIWorkflowService.FindVersionPageByParams(queryParams)
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: builders.BuildAIWorkflowVersionList(list), Page: paging})
|
||||
}
|
||||
|
||||
func AIWorkflowGetVersionBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item := services.AIWorkflowService.GetVersion(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0002"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAIWorkflowVersion(item))
|
||||
}
|
||||
|
||||
func AIWorkflowAnyRunList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "workflowId"},
|
||||
params.QueryFilter{ParamName: "workflowVersionId"},
|
||||
params.QueryFilter{ParamName: "conversationId"},
|
||||
params.QueryFilter{ParamName: "aiAgentId"},
|
||||
params.QueryFilter{ParamName: "messageId"},
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
).Desc("id")
|
||||
list, paging := services.AIWorkflowService.FindRunPageByCnd(cnd)
|
||||
auditItems := services.AIWorkflowService.BuildRunAuditItems(list)
|
||||
results := make([]response.AIWorkflowRunResponse, 0, len(auditItems))
|
||||
for i := range auditItems {
|
||||
item := auditItems[i]
|
||||
results = append(results, builders.BuildAIWorkflowRunWithContext(&item.Run, item.Workflow, item.Version, item.Agent))
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func AIWorkflowGetRunBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAIAgentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, nodes := services.AIWorkflowService.GetRunDetail(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0002"))
|
||||
return
|
||||
}
|
||||
auditItems := services.AIWorkflowService.BuildRunAuditItems([]models.AIWorkflowRun{*item})
|
||||
if len(auditItems) == 0 {
|
||||
httpx.WriteJSON(ctx, builders.BuildAIWorkflowRunDetail(item, nodes))
|
||||
return
|
||||
}
|
||||
auditItem := auditItems[0]
|
||||
httpx.WriteJSON(ctx, builders.BuildAIWorkflowRunDetailWithContext(&auditItem.Run, nodes, auditItem.Workflow, auditItem.Version, auditItem.Agent))
|
||||
}
|
||||
102
internal/handlers/dashboard/asset_handler.go
Normal file
102
internal/handlers/dashboard/asset_handler.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func AssetAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAssetView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "provider"},
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "createUserId"},
|
||||
params.QueryFilter{ParamName: "filename", Op: params.Like},
|
||||
).Desc("id")
|
||||
if strings.TrimSpace(ctx.Query("status")) == "" {
|
||||
cnd = cnd.Eq("status", enums.AssetStatusSuccess)
|
||||
}
|
||||
|
||||
list, paging := services.AssetService.FindPageByCnd(cnd)
|
||||
results := make([]response.AssetResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, builders.BuildAsset(&item))
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func AssetGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionAssetView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item := services.AssetService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0214"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAsset(item))
|
||||
}
|
||||
|
||||
func AssetPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAssetCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.CreateAssetRequest{}
|
||||
if err := params.ReadForm(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
header, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0323"))
|
||||
return
|
||||
}
|
||||
item, err := services.AssetService.UploadFile(header, req.Prefix, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAsset(item))
|
||||
}
|
||||
|
||||
func AssetPostDelete(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionAssetDelete)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.DeleteAssetRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.AssetService.DeleteAsset(req.ID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
53
internal/handlers/dashboard/branding_handler.go
Normal file
53
internal/handlers/dashboard/branding_handler.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func BrandingGetConfig(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionBrandingView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SystemConfigService.GetDashboardBranding())
|
||||
}
|
||||
|
||||
// BrandingPostSave renames the deployment. The name and logo apply to the
|
||||
// dashboard, the visitor portal and the widget chrome alike, because all three
|
||||
// read the same resolved branding.
|
||||
func BrandingPostSave(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionBrandingUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := map[string]json.RawMessage{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
branding, err := services.SystemConfigService.SaveBranding(req, operator)
|
||||
if err != nil {
|
||||
if validationErr, ok := err.(*services.SystemConfigValidationError); ok {
|
||||
locale := i18nx.Locale(ctx)
|
||||
httpx.WriteJSON(ctx, web.JsonErrorData(errorsx.CodeInvalidParam, validationErr.Message(locale), gin.H{
|
||||
"errors": validationErr.FieldErrorsLocale(locale),
|
||||
}))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, branding)
|
||||
}
|
||||
172
internal/handlers/dashboard/channel_handler.go
Normal file
172
internal/handlers/dashboard/channel_handler.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func ChannelAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionChannelView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list, paging := services.ChannelService.FindPageByCnd(params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "name", Op: params.Like},
|
||||
params.QueryFilter{ParamName: "channelType"},
|
||||
params.QueryFilter{ParamName: "channelId", Op: params.Like},
|
||||
).Where("status <> ?", enums.StatusDeleted).Desc("id"))
|
||||
results := make([]response.ChannelResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, buildChannelResponse(&item))
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func ChannelGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionChannelView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item := services.ChannelService.Get(id)
|
||||
if item == nil || item.Status == enums.StatusDeleted {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0062"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, buildChannelResponse(item))
|
||||
}
|
||||
|
||||
func ChannelPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionChannelCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateChannelRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.ChannelService.CreateChannel(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, buildChannelResponse(item))
|
||||
}
|
||||
|
||||
func ChannelPostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionChannelUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateChannelRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ChannelService.UpdateChannel(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ChannelPostRollback_ai_agent_rollout(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionChannelUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.RollbackChannelAIAgentRolloutRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ChannelService.RollbackChannelAIAgentRollout(req.ID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ChannelPostUpdate_status(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionChannelUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateChannelStatusRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ChannelService.UpdateStatus(req.ID, req.Status, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ChannelPostReset_user_token_secret(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionChannelUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.ResetChannelUserTokenSecretRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
secret, err := services.ChannelService.ResetUserTokenSecret(req.ID, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, map[string]string{"userTokenSecret": secret})
|
||||
}
|
||||
|
||||
func ChannelPostDelete(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionChannelDelete)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteChannelRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ChannelService.DeleteChannel(req.ID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func buildChannelResponse(item *models.Channel) response.ChannelResponse {
|
||||
ret := response.BuildChannelResponse(item)
|
||||
if item == nil {
|
||||
return ret
|
||||
}
|
||||
if aiAgent := services.AIAgentService.Get(item.AIAgentID); aiAgent != nil {
|
||||
ret.AIAgentName = aiAgent.Name
|
||||
}
|
||||
return ret
|
||||
}
|
||||
44
internal/handlers/dashboard/channel_test_email.go
Normal file
44
internal/handlers/dashboard/channel_test_email.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ChannelPostTest_email checks that the channel's bound mail server is reachable
|
||||
// and will accept its credentials, without sending any mail.
|
||||
//
|
||||
// It exists so binding a channel to an SMTP server is a verified action. The
|
||||
// alternative is finding out that a host, port or password is wrong by filing a
|
||||
// real request and watching the outbox retry five times.
|
||||
func ChannelPostTest_email(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionChannelUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.IDRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if req.ID <= 0 {
|
||||
httpx.WriteJSON(ctx, errorsx.InvalidParamI18n("error.email.channelIdRequired"))
|
||||
return
|
||||
}
|
||||
|
||||
// The probe failure message carries the transport detail, which is the
|
||||
// actionable part for whoever bound the server.
|
||||
result, err := services.EmailDeliveryService.TestChannelSMTP(ctx.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, result)
|
||||
}
|
||||
130
internal/handlers/dashboard/company_handler.go
Normal file
130
internal/handlers/dashboard/company_handler.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func CompanyAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCompanyView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list, paging := services.CompanyService.FindPageByCnd(params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "name", Op: params.Like},
|
||||
params.QueryFilter{ParamName: "code", Op: params.Like},
|
||||
).Where("status <> ?", enums.StatusDeleted).Desc("id"))
|
||||
|
||||
results := builders.BuildCompanyList(list)
|
||||
companyIDs := make([]int64, 0, len(results))
|
||||
for _, item := range results {
|
||||
companyIDs = append(companyIDs, item.ID)
|
||||
}
|
||||
countMap := services.CustomerService.CountByCompanyIDs(companyIDs)
|
||||
for i := range results {
|
||||
results[i].CustomerCount = countMap[results[i].ID]
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func CompanyGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCompanyView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item := services.CompanyService.Get(id)
|
||||
if item == nil || item.Status == enums.StatusDeleted {
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
return
|
||||
}
|
||||
ret := builders.BuildCompany(item)
|
||||
httpx.WriteJSON(ctx, &ret)
|
||||
}
|
||||
|
||||
func CompanyPostCreate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionCompanyCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateCompanyRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.CompanyService.CreateCompany(req, user)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
ret := builders.BuildCompany(item)
|
||||
httpx.WriteJSON(ctx, &ret)
|
||||
}
|
||||
|
||||
func CompanyPostUpdate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionCompanyUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateCompanyRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.CompanyService.UpdateCompany(req, user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func CompanyPostDelete(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionCompanyDelete)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteCompanyRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.CompanyService.DeleteCompany(req.ID, *user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func CompanyPostUpdate_status(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionCompanyUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateCompanyStatusRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.CompanyService.UpdateStatus(req.ID, req.Status, user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
465
internal/handlers/dashboard/conversation_handler.go
Normal file
465
internal/handlers/dashboard/conversation_handler.go
Normal file
@@ -0,0 +1,465 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/common/strs"
|
||||
"github.com/mlogclub/simple/web"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
func ConversationAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "serviceMode"},
|
||||
params.QueryFilter{ParamName: "currentAssigneeId"},
|
||||
).Desc("last_message_at").Desc("id")
|
||||
|
||||
paging := params.GetPaging(ctx)
|
||||
|
||||
if keyword, _ := params.Get(ctx, "keyword"); strs.IsNotBlank(keyword) {
|
||||
keywordLike := "%" + strings.TrimSpace(keyword) + "%"
|
||||
cnd.Where("customer_name LIKE ? OR last_message_summary LIKE ?", keywordLike, keywordLike)
|
||||
}
|
||||
|
||||
// 标签搜索
|
||||
if tagID, _ := params.GetInt64(ctx, "tagId"); tagID > 0 {
|
||||
tagIDs := services.TagService.GetSelfAndDescendantIDs(tagID)
|
||||
if len(tagIDs) == 0 {
|
||||
httpx.WriteJSON(ctx, &web.PageResult{
|
||||
Results: []response.ConversationResponse{},
|
||||
Page: paging,
|
||||
})
|
||||
return
|
||||
}
|
||||
cnd.Where("id IN (SELECT conversation_id FROM conversation_tag_rels WHERE tag_id IN (?))", tagIDs)
|
||||
}
|
||||
if agentTeamID, _ := params.GetInt64(ctx, "agentTeamId"); agentTeamID > 0 {
|
||||
userIDs := services.AgentProfileService.GetUserIDsByTeamID(agentTeamID)
|
||||
if len(userIDs) == 0 {
|
||||
httpx.WriteJSON(ctx, &web.PageResult{
|
||||
Results: []response.ConversationResponse{},
|
||||
Page: paging,
|
||||
})
|
||||
return
|
||||
}
|
||||
cnd.In("current_assignee_id", userIDs)
|
||||
}
|
||||
|
||||
list, paging := services.ConversationService.FindPageByCnd(cnd)
|
||||
results := make([]response.ConversationResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, builders.BuildConversationWithLocale(&item, i18nx.Locale(ctx)))
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func ConversationAnyConversations(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationView)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
filterValue, _ := params.Get(ctx, "filter")
|
||||
keyword, _ := params.Get(ctx, "keyword")
|
||||
paging := params.GetPaging(ctx)
|
||||
|
||||
list, paging, err := services.ConversationService.ListConversations(
|
||||
operator.UserID,
|
||||
request.AgentConversationFilter(strings.TrimSpace(filterValue)),
|
||||
keyword,
|
||||
paging,
|
||||
)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
results := make([]response.ConversationResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, builders.BuildConversationWithLocale(&item, i18nx.Locale(ctx)))
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func ConversationGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := services.ConversationService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0116"))
|
||||
return
|
||||
}
|
||||
|
||||
detail := response.ConversationDetailResponse{
|
||||
ConversationResponse: builders.BuildConversationWithLocale(item, i18nx.Locale(ctx)),
|
||||
Participants: builders.BuildParticipantResponses(id),
|
||||
}
|
||||
httpx.WriteJSON(ctx, detail)
|
||||
}
|
||||
|
||||
func ConversationAnyMessage_list(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
conversationID, _ = params.GetInt64(ctx, "conversationId")
|
||||
senderType, _ = params.Get(ctx, "senderType")
|
||||
messageType, _ = params.Get(ctx, "messageType")
|
||||
cursor, _ = params.GetInt64(ctx, "cursor")
|
||||
limit, _ = params.GetInt(ctx, "limit")
|
||||
)
|
||||
if conversation := services.ConversationService.Get(conversationID); conversation == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0116"))
|
||||
return
|
||||
}
|
||||
|
||||
list, nextCursor, hasMore := services.MessageService.FindByConversationIDCursor(
|
||||
conversationID, cursor, limit, senderType, messageType,
|
||||
)
|
||||
results := builders.BuildMessagesWithLocale(list, i18nx.Locale(ctx))
|
||||
|
||||
httpx.WriteJSON(ctx, httpx.CursorData(results, cast.ToString(nextCursor), hasMore))
|
||||
}
|
||||
|
||||
func ConversationPostAssign(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationAssign)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.AssignConversationRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ConversationService.AssignConversation(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ConversationPostDispatch(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationAssign)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.DispatchConversationRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ConversationService.AutoAssignConversation(req.ConversationID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ConversationPostTransfer(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationTransfer)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.TransferConversationRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ConversationService.TransferConversation(req.ConversationID, req.ToUserID, req.Reason, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ConversationPostClose(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationClose)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.CloseConversationRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ConversationService.CloseConversation(req.ConversationID, req.CloseReason, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ConversationPostLink_customer(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationLinkCustomer)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.LinkConversationCustomerRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ConversationService.LinkConversationCustomer(req.ConversationID, req.CustomerID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ConversationPostSend_message(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationSend)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.SendConversationMessageRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.MessageService.SendAgentMessageWithRequestID(req.ConversationID, 0, req.ClientMsgID, req.MessageType, req.Content, req.Payload, operator, httpx.GetRequestID(ctx))
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildMessageWithLocale(item, i18nx.Locale(ctx)))
|
||||
}
|
||||
|
||||
func ConversationPostRecall_message(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationSend)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.RecallConversationMessageRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.MessageService.RecallAgentMessage(req.MessageID, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildMessageWithLocale(item, i18nx.Locale(ctx)))
|
||||
}
|
||||
|
||||
func ConversationPostRead(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationView)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.ReadConversationRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ConversationService.MarkAgentConversationReadToMessage(req.ConversationID, req.MessageID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ConversationPostUpload_image(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationSend)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
rawConv := strings.TrimSpace(params.FormValue(ctx, "conversationId"))
|
||||
if rawConv == "" {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064"))
|
||||
return
|
||||
}
|
||||
conversationID, err := strconv.ParseInt(rawConv, 10, 64)
|
||||
if err != nil || conversationID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064"))
|
||||
return
|
||||
}
|
||||
if _, err := services.MessageService.ValidateConversationSender(conversationID, enums.IMSenderTypeAgent, operator, nil); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
header, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0322"))
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(strings.ToLower(header.Header.Get("Content-Type")), "image/") {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0090"))
|
||||
return
|
||||
}
|
||||
|
||||
item, err := services.AssetService.UploadFile(header, "images", operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAsset(item))
|
||||
}
|
||||
|
||||
func ConversationPostUpload_attachment(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationSend)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
rawConv := strings.TrimSpace(params.FormValue(ctx, "conversationId"))
|
||||
if rawConv == "" {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064"))
|
||||
return
|
||||
}
|
||||
conversationID, err := strconv.ParseInt(rawConv, 10, 64)
|
||||
if err != nil || conversationID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064"))
|
||||
return
|
||||
}
|
||||
if _, err := services.MessageService.ValidateConversationSender(conversationID, enums.IMSenderTypeAgent, operator, nil); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
header, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0324"))
|
||||
return
|
||||
}
|
||||
item, err := services.AssetService.UploadFile(header, "attachments", operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildAsset(item))
|
||||
}
|
||||
|
||||
func ConversationPostAdd_tag(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationTag)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.AddConversationTagRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ConversationTagService.AddTag(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ConversationPostRemove_tag(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationTag); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.RemoveConversationTagRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.ConversationTagService.RemoveTag(req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func ConversationPostSuggest_reply(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.SuggestReplyRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if req.ConversationID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064"))
|
||||
return
|
||||
}
|
||||
|
||||
suggestion, err := services.AICopilotService.SuggestReply(ctx.Request.Context(), req.ConversationID, i18nx.Locale(ctx))
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpx.WriteJSON(ctx, &response.SuggestReplyResponse{Suggestion: suggestion})
|
||||
}
|
||||
|
||||
func ConversationPostSummarize(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionConversationView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.SummarizeConversationRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if req.ConversationID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0064"))
|
||||
return
|
||||
}
|
||||
|
||||
summary, err := services.AICopilotService.SummarizeConversation(ctx.Request.Context(), req.ConversationID, i18nx.Locale(ctx))
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpx.WriteJSON(ctx, &response.ConversationSummaryResponse{Summary: summary})
|
||||
}
|
||||
313
internal/handlers/dashboard/conversation_handler_test.go
Normal file
313
internal/handlers/dashboard/conversation_handler_test.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"agent-desk/internal/ai"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
func setupConversationDashboardTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
|
||||
db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"), &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: "t_",
|
||||
SingularTable: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if sqlDB, err := db.DB(); err == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
})
|
||||
|
||||
if err := db.AutoMigrate(
|
||||
&models.Conversation{},
|
||||
&models.Message{},
|
||||
&models.User{},
|
||||
); err != nil {
|
||||
t.Fatalf("auto migrate error = %v", err)
|
||||
}
|
||||
sqls.SetDB(db)
|
||||
return db
|
||||
}
|
||||
|
||||
func TestConversationSuggestReply(t *testing.T) {
|
||||
db := setupConversationDashboardTestDB(t)
|
||||
|
||||
conv := &models.Conversation{
|
||||
ID: 101,
|
||||
Subject: "Suggest Reply Test",
|
||||
}
|
||||
if err := db.Create(conv).Error; err != nil {
|
||||
t.Fatalf("create conv error = %v", err)
|
||||
}
|
||||
msg := &models.Message{
|
||||
ID: 1,
|
||||
ConversationID: conv.ID,
|
||||
ClientMsgID: "msg_1",
|
||||
SenderType: enums.IMSenderTypeCustomer,
|
||||
MessageType: enums.IMMessageTypeText,
|
||||
Content: "Can you help me reset my password?",
|
||||
}
|
||||
if err := db.Create(msg).Error; err != nil {
|
||||
t.Fatalf("create msg error = %v", err)
|
||||
}
|
||||
|
||||
t.Run("unauthorized when no auth principal", func(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.POST("/api/dashboard/conversation/suggest_reply", ConversationPostSuggest_reply)
|
||||
|
||||
body, _ := json.Marshal(request.SuggestReplyRequest{ConversationID: conv.ID})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/suggest_reply", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["success"] == true {
|
||||
t.Fatalf("expected failure without auth, got success: true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("forbidden when principal lacks permission", func(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set("authPrincipal", &dto.AuthPrincipal{
|
||||
UserID: 1,
|
||||
Username: "agent",
|
||||
Permissions: []string{"unrelated.permission"},
|
||||
})
|
||||
c.Next()
|
||||
})
|
||||
router.POST("/api/dashboard/conversation/suggest_reply", ConversationPostSuggest_reply)
|
||||
|
||||
body, _ := json.Marshal(request.SuggestReplyRequest{ConversationID: conv.ID})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/suggest_reply", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["success"] == true {
|
||||
t.Fatalf("expected forbidden error, got success: true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid params when conversationId <= 0", func(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set("authPrincipal", &dto.AuthPrincipal{
|
||||
UserID: 1,
|
||||
Username: "agent",
|
||||
Permissions: []string{constants.PermissionConversationView.Code},
|
||||
})
|
||||
c.Next()
|
||||
})
|
||||
router.POST("/api/dashboard/conversation/suggest_reply", ConversationPostSuggest_reply)
|
||||
|
||||
body, _ := json.Marshal(request.SuggestReplyRequest{ConversationID: 0})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/suggest_reply", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["success"] == true {
|
||||
t.Fatalf("expected error for conversationId <= 0, got success: true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successful suggestion", func(t *testing.T) {
|
||||
restore := services.AICopilotService.SetChatFnForTest(func(ctx context.Context, systemPrompt, userPrompt string) (*ai.ChatCompletionResult, error) {
|
||||
return &ai.ChatCompletionResult{
|
||||
Content: "Click 'Forgot Password' to reset your credentials.",
|
||||
ModelName: "mock-model",
|
||||
}, nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
router := gin.New()
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set("authPrincipal", &dto.AuthPrincipal{
|
||||
UserID: 1,
|
||||
Username: "agent",
|
||||
Permissions: []string{constants.PermissionConversationView.Code},
|
||||
})
|
||||
c.Next()
|
||||
})
|
||||
router.POST("/api/dashboard/conversation/suggest_reply", ConversationPostSuggest_reply)
|
||||
|
||||
body, _ := json.Marshal(request.SuggestReplyRequest{ConversationID: conv.ID})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/suggest_reply", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Code int `json:"code"`
|
||||
Success bool `json:"success"`
|
||||
Data response.SuggestReplyResponse `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal response error = %v", err)
|
||||
}
|
||||
if !resp.Success {
|
||||
t.Fatalf("expected success: true, got false. Body: %s", rec.Body.String())
|
||||
}
|
||||
expectedSuggestion := "Click 'Forgot Password' to reset your credentials."
|
||||
if resp.Data.Suggestion != expectedSuggestion {
|
||||
t.Fatalf("expected suggestion %q, got %q", expectedSuggestion, resp.Data.Suggestion)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConversationSummarize(t *testing.T) {
|
||||
db := setupConversationDashboardTestDB(t)
|
||||
|
||||
conv := &models.Conversation{
|
||||
ID: 201,
|
||||
Subject: "Summarize Test",
|
||||
}
|
||||
if err := db.Create(conv).Error; err != nil {
|
||||
t.Fatalf("create conv error = %v", err)
|
||||
}
|
||||
msg := &models.Message{
|
||||
ID: 20,
|
||||
ConversationID: conv.ID,
|
||||
ClientMsgID: "msg_20",
|
||||
SenderType: enums.IMSenderTypeCustomer,
|
||||
MessageType: enums.IMMessageTypeText,
|
||||
Content: "Billing dispute for invoice #1234",
|
||||
}
|
||||
if err := db.Create(msg).Error; err != nil {
|
||||
t.Fatalf("create msg error = %v", err)
|
||||
}
|
||||
|
||||
t.Run("unauthorized when no auth principal", func(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.POST("/api/dashboard/conversation/summarize", ConversationPostSummarize)
|
||||
|
||||
body, _ := json.Marshal(request.SummarizeConversationRequest{ConversationID: conv.ID})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/summarize", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["success"] == true {
|
||||
t.Fatalf("expected failure without auth, got success: true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid params when conversationId <= 0", func(t *testing.T) {
|
||||
router := gin.New()
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set("authPrincipal", &dto.AuthPrincipal{
|
||||
UserID: 1,
|
||||
Username: "agent",
|
||||
Permissions: []string{constants.PermissionConversationView.Code},
|
||||
})
|
||||
c.Next()
|
||||
})
|
||||
router.POST("/api/dashboard/conversation/summarize", ConversationPostSummarize)
|
||||
|
||||
body, _ := json.Marshal(request.SummarizeConversationRequest{ConversationID: -5})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/summarize", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["success"] == true {
|
||||
t.Fatalf("expected error for conversationId <= 0, got success: true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("successful summary", func(t *testing.T) {
|
||||
restore := services.AICopilotService.SetChatFnForTest(func(ctx context.Context, systemPrompt, userPrompt string) (*ai.ChatCompletionResult, error) {
|
||||
return &ai.ChatCompletionResult{
|
||||
Content: "Summary: Customer inquired about invoice #1234.",
|
||||
ModelName: "mock-model",
|
||||
}, nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
router := gin.New()
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Set("authPrincipal", &dto.AuthPrincipal{
|
||||
UserID: 1,
|
||||
Username: "agent",
|
||||
Permissions: []string{constants.PermissionConversationView.Code},
|
||||
})
|
||||
c.Next()
|
||||
})
|
||||
router.POST("/api/dashboard/conversation/summarize", ConversationPostSummarize)
|
||||
|
||||
body, _ := json.Marshal(request.SummarizeConversationRequest{ConversationID: conv.ID})
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/dashboard/conversation/summarize", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Code int `json:"code"`
|
||||
Success bool `json:"success"`
|
||||
Data response.ConversationSummaryResponse `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("unmarshal response error = %v", err)
|
||||
}
|
||||
if !resp.Success {
|
||||
t.Fatalf("expected success: true, got false. Body: %s", rec.Body.String())
|
||||
}
|
||||
expectedSummary := "Summary: Customer inquired about invoice #1234."
|
||||
if resp.Data.Summary != expectedSummary {
|
||||
t.Fatalf("expected summary %q, got %q", expectedSummary, resp.Data.Summary)
|
||||
}
|
||||
})
|
||||
}
|
||||
84
internal/handlers/dashboard/customer_contact_handler.go
Normal file
84
internal/handlers/dashboard/customer_contact_handler.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AnyList GET/POST /customer-contact/list?customerId=
|
||||
func CustomerContactAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCustomerView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
customerID, _ := params.GetInt64(ctx, "customerId")
|
||||
if customerID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0065"))
|
||||
return
|
||||
}
|
||||
list := services.CustomerContactService.FindActiveByCustomerID(customerID)
|
||||
httpx.WriteJSON(ctx, builders.BuildCustomerContactList(list))
|
||||
}
|
||||
|
||||
func CustomerContactPostCreate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionCustomerUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateCustomerContactRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.CustomerContactService.CreateCustomerContact(req, user)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
ret := builders.BuildCustomerContactResponse(item)
|
||||
httpx.WriteJSON(ctx, &ret)
|
||||
}
|
||||
|
||||
func CustomerContactPostUpdate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionCustomerUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateCustomerContactRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.CustomerContactService.UpdateCustomerContact(req, user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func CustomerContactPostDelete(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionCustomerUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteCustomerContactRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.CustomerContactService.DeleteCustomerContact(req.ID, user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
150
internal/handlers/dashboard/customer_handler.go
Normal file
150
internal/handlers/dashboard/customer_handler.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func CustomerPostList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCustomerView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
var req request.CustomerListRequest
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list, paging := services.CustomerService.ListCustomers(req)
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: builders.BuildCustomerList(list), Page: paging})
|
||||
}
|
||||
|
||||
func CustomerGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCustomerView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item := services.CustomerService.Get(id)
|
||||
if item == nil || item.Status == enums.StatusDeleted {
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
return
|
||||
}
|
||||
ret := builders.BuildCustomer(item)
|
||||
httpx.WriteJSON(ctx, &ret)
|
||||
}
|
||||
|
||||
// PostSave_profile POST /save_profile — 客户主信息与联系方式在同一事务中保存。
|
||||
func CustomerPostSave_profile(ctx *gin.Context) {
|
||||
req := request.SaveCustomerProfileRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
createMode := req.ID == nil || *req.ID <= 0
|
||||
var user *dto.AuthPrincipal
|
||||
var err error
|
||||
if createMode {
|
||||
user, err = services.AuthService.RequirePermission(ctx, constants.PermissionCustomerCreate)
|
||||
} else {
|
||||
user, err = services.AuthService.RequirePermission(ctx, constants.PermissionCustomerUpdate)
|
||||
}
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.CustomerService.SaveCustomerProfile(req, user)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
ret := builders.BuildCustomer(item)
|
||||
httpx.WriteJSON(ctx, &ret)
|
||||
}
|
||||
|
||||
func CustomerPostCreate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionCustomerCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateCustomerRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.CustomerService.CreateCustomer(req, user)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
ret := builders.BuildCustomer(item)
|
||||
httpx.WriteJSON(ctx, &ret)
|
||||
}
|
||||
|
||||
func CustomerPostUpdate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionCustomerUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateCustomerRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.CustomerService.UpdateCustomer(req, user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func CustomerPostDelete(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionCustomerDelete)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteCustomerRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.CustomerService.DeleteCustomer(req.ID, *user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func CustomerPostUpdate_status(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionCustomerUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateCustomerStatusRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.CustomerService.UpdateStatus(req.ID, req.Status, user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
16
internal/handlers/dashboard/dashboard_handler.go
Normal file
16
internal/handlers/dashboard/dashboard_handler.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func DashboardGetOverview(ctx *gin.Context) {
|
||||
rangeValue, _ := params.Get(ctx, "range")
|
||||
httpx.WriteJSON(ctx, services.DashboardService.GetOverview(rangeValue, i18nx.Locale(ctx)))
|
||||
}
|
||||
184
internal/handlers/dashboard/knowledge_base_handler.go
Normal file
184
internal/handlers/dashboard/knowledge_base_handler.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"agent-desk/internal/ai/rag"
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/repositories"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func KnowledgeBaseAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "name", Op: params.Like},
|
||||
).Asc("sort_no").Desc("id")
|
||||
list, paging := services.KnowledgeBaseService.FindPageByCnd(cnd)
|
||||
results := make([]response.KnowledgeBaseResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
docCount := repositories.KnowledgeDocumentRepository.CountByKnowledgeBaseID(sqls.DB(), item.ID)
|
||||
faqCount := repositories.KnowledgeFAQRepository.CountByKnowledgeBaseID(sqls.DB(), item.ID)
|
||||
resp := builders.BuildKnowledgeBase(&item)
|
||||
resp.DocumentCount = docCount
|
||||
resp.FAQCount = faqCount
|
||||
results = append(results, resp)
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func KnowledgeBaseAnyList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
list := services.KnowledgeBaseService.Find(params.NewSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
).Asc("sort_no").Desc("id"))
|
||||
results := make([]response.KnowledgeBaseResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
resp := builders.BuildKnowledgeBase(&item)
|
||||
results = append(results, resp)
|
||||
}
|
||||
httpx.WriteJSON(ctx, results)
|
||||
}
|
||||
|
||||
func KnowledgeBaseGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := services.KnowledgeBaseService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0283"))
|
||||
return
|
||||
}
|
||||
resp := builders.BuildKnowledgeBase(item)
|
||||
resp.DocumentCount = repositories.KnowledgeDocumentRepository.CountByKnowledgeBaseID(sqls.DB(), item.ID)
|
||||
resp.FAQCount = repositories.KnowledgeFAQRepository.CountByKnowledgeBaseID(sqls.DB(), item.ID)
|
||||
httpx.WriteJSON(ctx, resp)
|
||||
}
|
||||
|
||||
func KnowledgeBasePostCreate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.CreateKnowledgeBaseRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.KnowledgeBaseService.CreateKnowledgeBase(req, user)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildKnowledgeBase(item))
|
||||
}
|
||||
|
||||
func KnowledgeBasePostUpdate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateKnowledgeBaseRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeBaseService.UpdateKnowledgeBase(req, user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func KnowledgeBasePostDelete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeBaseService.DeleteKnowledgeBase(req.ID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func KnowledgeBasePostUpdate_sort(ctx *gin.Context) {
|
||||
var ids []int64
|
||||
if err := params.ReadJSON(ctx, &ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeBaseService.UpdateSort(ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func KnowledgeBasePostRebuild_index(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
knowledgeBase := services.KnowledgeBaseService.Get(req.ID)
|
||||
if knowledgeBase == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0283"))
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
if err := rag.Index.RebuildKnowledgeBaseIndex(ctx, req.ID); err != nil {
|
||||
slog.Error("Failed to rebuild knowledge base index", "knowledge_base_id", req.ID, "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
124
internal/handlers/dashboard/knowledge_directory_handler.go
Normal file
124
internal/handlers/dashboard/knowledge_directory_handler.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func KnowledgeDirectoryGetList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
knowledgeBaseID, ok := params.GetInt64(ctx, "knowledgeBaseId")
|
||||
if !ok || knowledgeBaseID <= 0 {
|
||||
httpx.WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0283"))
|
||||
return
|
||||
}
|
||||
list := services.KnowledgeDirectoryService.FindAllByKnowledgeBaseID(knowledgeBaseID)
|
||||
httpx.WriteJSON(ctx, buildKnowledgeDirectoryTreeResponses(list))
|
||||
}
|
||||
|
||||
func KnowledgeDirectoryPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateKnowledgeDirectoryRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.KnowledgeDirectoryService.CreateDirectory(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
resp := builders.BuildKnowledgeDirectory(item)
|
||||
httpx.WriteJSON(ctx, &resp)
|
||||
}
|
||||
|
||||
func KnowledgeDirectoryPostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateKnowledgeDirectoryRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeDirectoryService.UpdateDirectory(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func KnowledgeDirectoryPostDelete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteKnowledgeDirectoryRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeDirectoryService.DeleteDirectory(req.ID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func KnowledgeDirectoryPostUpdate_sort(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeBaseUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
KnowledgeBaseID int64 `json:"knowledgeBaseId"`
|
||||
ParentID int64 `json:"parentId"`
|
||||
IDs []int64 `json:"ids"`
|
||||
}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeDirectoryService.UpdateSort(req.KnowledgeBaseID, req.ParentID, req.IDs); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func buildKnowledgeDirectoryTreeResponses(list []models.KnowledgeDirectory) []response.KnowledgeDirectoryResponse {
|
||||
childrenByParent := make(map[int64][]models.KnowledgeDirectory, len(list))
|
||||
for _, item := range list {
|
||||
childrenByParent[item.ParentID] = append(childrenByParent[item.ParentID], item)
|
||||
}
|
||||
var build func(parentID int64) []response.KnowledgeDirectoryResponse
|
||||
build = func(parentID int64) []response.KnowledgeDirectoryResponse {
|
||||
items := childrenByParent[parentID]
|
||||
ret := make([]response.KnowledgeDirectoryResponse, 0, len(items))
|
||||
for _, item := range items {
|
||||
resp := builders.BuildKnowledgeDirectory(&item)
|
||||
resp.Children = build(item.ID)
|
||||
ret = append(ret, resp)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
return build(0)
|
||||
}
|
||||
181
internal/handlers/dashboard/knowledge_document_handler.go
Normal file
181
internal/handlers/dashboard/knowledge_document_handler.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func KnowledgeDocumentAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "knowledgeBaseId"},
|
||||
params.QueryFilter{ParamName: "title", Op: params.Like},
|
||||
).Desc("id")
|
||||
knowledgeBaseID, _ := params.GetInt64(ctx, "knowledgeBaseId")
|
||||
if directoryID, ok := params.GetInt64(ctx, "directoryId"); ok {
|
||||
cnd.Where("directory_id = ?", directoryID)
|
||||
}
|
||||
|
||||
if status, ok := params.GetInt64(ctx, "status"); ok {
|
||||
cnd.Where("status = ?", status)
|
||||
} else {
|
||||
cnd.Where("status != ?", enums.StatusDeleted)
|
||||
}
|
||||
if indexStatus, ok := params.Get(ctx, "indexStatus"); ok {
|
||||
if !enums.IsValidKnowledgeDocumentIndexStatus(indexStatus) {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0067"))
|
||||
return
|
||||
}
|
||||
cnd.Where("index_status = ?", indexStatus)
|
||||
}
|
||||
|
||||
list, paging := services.KnowledgeDocumentService.FindPageListByCnd(cnd)
|
||||
directoryPaths := services.KnowledgeDirectoryService.PathMap(knowledgeBaseID)
|
||||
results := make([]response.KnowledgeDocumentListResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
resp := builders.BuildKnowledgeDocumentList(&item)
|
||||
fillKnowledgeDocumentDirectory(&resp, directoryPaths)
|
||||
results = append(results, resp)
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func KnowledgeDocumentGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := services.KnowledgeDocumentService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0218"))
|
||||
return
|
||||
}
|
||||
resp := builders.BuildKnowledgeDocument(item)
|
||||
fillKnowledgeDocumentDirectory(&resp, services.KnowledgeDirectoryService.PathMap(item.KnowledgeBaseID))
|
||||
httpx.WriteJSON(ctx, resp)
|
||||
}
|
||||
|
||||
func fillKnowledgeDocumentDirectory(resp any, directoryPaths map[int64]string) {
|
||||
switch item := resp.(type) {
|
||||
case *response.KnowledgeDocumentResponse:
|
||||
item.DirectoryPath = directoryPaths[item.DirectoryID]
|
||||
item.DirectoryName = item.DirectoryPath
|
||||
case *response.KnowledgeDocumentListResponse:
|
||||
item.DirectoryPath = directoryPaths[item.DirectoryID]
|
||||
item.DirectoryName = item.DirectoryPath
|
||||
}
|
||||
}
|
||||
|
||||
func KnowledgeDocumentPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.CreateKnowledgeDocumentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.KnowledgeDocumentService.CreateKnowledgeDocument(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildKnowledgeDocument(item))
|
||||
}
|
||||
|
||||
func KnowledgeDocumentPostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateKnowledgeDocumentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeDocumentService.UpdateKnowledgeDocument(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func KnowledgeDocumentPostDelete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.KnowledgeDocumentService.DeleteKnowledgeDocument(req.ID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func KnowledgeDocumentPostBatch_move(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.BatchMoveKnowledgeDocumentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeDocumentService.BatchMoveKnowledgeDocuments(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func KnowledgeDocumentPostBatch_delete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.BatchDeleteKnowledgeDocumentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeDocumentService.BatchDeleteKnowledgeDocuments(req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
241
internal/handlers/dashboard/knowledge_faq_handler.go
Normal file
241
internal/handlers/dashboard/knowledge_faq_handler.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func KnowledgeFAQGetImport_template(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
file, err := services.KnowledgeFAQService.BuildKnowledgeFAQImportTemplate()
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
writeKnowledgeFAQExcelFile(ctx, file)
|
||||
}
|
||||
|
||||
func KnowledgeFAQGetExport(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
knowledgeBaseID, ok := params.GetInt64(ctx, "knowledgeBaseId")
|
||||
if !ok || knowledgeBaseID <= 0 {
|
||||
httpx.WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0283"))
|
||||
return
|
||||
}
|
||||
file, err := services.KnowledgeFAQService.ExportKnowledgeFAQs(knowledgeBaseID)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
writeKnowledgeFAQExcelFile(ctx, file)
|
||||
}
|
||||
|
||||
func KnowledgeFAQPostImport(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
mode := request.KnowledgeFAQImportMode(ctx.PostForm("mode"))
|
||||
if mode == request.KnowledgeFAQImportModeOverwrite {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
knowledgeBaseID, ok := params.GetInt64(ctx, "knowledgeBaseId")
|
||||
if !ok || knowledgeBaseID <= 0 {
|
||||
httpx.WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0283"))
|
||||
return
|
||||
}
|
||||
header, err := ctx.FormFile("file")
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0327"))
|
||||
return
|
||||
}
|
||||
file, err := header.Open()
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, errorsx.InvalidParamI18n("error.e0176"))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
result, err := services.KnowledgeFAQService.ImportKnowledgeFAQs(request.ImportKnowledgeFAQRequest{
|
||||
KnowledgeBaseID: knowledgeBaseID,
|
||||
Mode: mode,
|
||||
Filename: header.Filename,
|
||||
Reader: file,
|
||||
Locale: i18nx.Locale(ctx),
|
||||
}, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, result)
|
||||
}
|
||||
|
||||
func KnowledgeFAQAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "knowledgeBaseId"},
|
||||
params.QueryFilter{ParamName: "question", Op: params.Like},
|
||||
params.QueryFilter{ParamName: "indexStatus"},
|
||||
).Desc("id")
|
||||
knowledgeBaseID, _ := params.GetInt64(ctx, "knowledgeBaseId")
|
||||
if directoryID, ok := params.GetInt64(ctx, "directoryId"); ok {
|
||||
cnd.Where("directory_id = ?", directoryID)
|
||||
}
|
||||
list, paging := services.KnowledgeFAQService.FindPageByCnd(cnd)
|
||||
directoryPaths := services.KnowledgeDirectoryService.PathMap(knowledgeBaseID)
|
||||
results := make([]response.KnowledgeFAQResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
resp := builders.BuildKnowledgeFAQ(&item)
|
||||
fillKnowledgeFAQDirectory(&resp, directoryPaths)
|
||||
results = append(results, resp)
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func KnowledgeFAQGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := services.KnowledgeFAQService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0025"))
|
||||
return
|
||||
}
|
||||
resp := builders.BuildKnowledgeFAQ(item)
|
||||
fillKnowledgeFAQDirectory(&resp, services.KnowledgeDirectoryService.PathMap(item.KnowledgeBaseID))
|
||||
httpx.WriteJSON(ctx, resp)
|
||||
}
|
||||
|
||||
func KnowledgeFAQPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateKnowledgeFAQRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.KnowledgeFAQService.CreateKnowledgeFAQ(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildKnowledgeFAQ(item))
|
||||
}
|
||||
|
||||
func KnowledgeFAQPostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateKnowledgeFAQRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeFAQService.UpdateKnowledgeFAQ(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func KnowledgeFAQPostDelete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeFAQService.DeleteKnowledgeFAQ(req.ID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func KnowledgeFAQPostBatch_move(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.BatchMoveKnowledgeFAQRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeFAQService.BatchMoveKnowledgeFAQs(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func KnowledgeFAQPostBatch_delete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.BatchDeleteKnowledgeFAQRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.KnowledgeFAQService.BatchDeleteKnowledgeFAQs(req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func writeKnowledgeFAQExcelFile(ctx *gin.Context, file *response.KnowledgeFAQExportedFile) {
|
||||
ctx.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, file.Filename))
|
||||
ctx.Header("Cache-Control", "no-store")
|
||||
ctx.Data(http.StatusOK, file.ContentType, file.Data)
|
||||
}
|
||||
|
||||
func fillKnowledgeFAQDirectory(resp *response.KnowledgeFAQResponse, directoryPaths map[int64]string) {
|
||||
resp.DirectoryPath = directoryPaths[resp.DirectoryID]
|
||||
resp.DirectoryName = resp.DirectoryPath
|
||||
}
|
||||
95
internal/handlers/dashboard/knowledge_retrieve_handler.go
Normal file
95
internal/handlers/dashboard/knowledge_retrieve_handler.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"context"
|
||||
|
||||
"agent-desk/internal/ai/rag"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func KnowledgeRetrievePostDebugSearch(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.KnowledgeSearchRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := rag.Answer.DebugSearch(context.Background(), req)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, resp)
|
||||
}
|
||||
|
||||
func KnowledgeRetrievePostDebugAnswer(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentView)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.KnowledgeAnswerRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := rag.Answer.DebugAnswer(context.Background(), req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, resp)
|
||||
}
|
||||
|
||||
func KnowledgeRetrievePostBuild(ctx *gin.Context) {
|
||||
req := struct {
|
||||
DocumentID int64 `json:"documentId"`
|
||||
FAQID int64 `json:"faqId"`
|
||||
}{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.DocumentID > 0 {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := rag.Answer.BuildDocumentIndex(context.Background(), req.DocumentID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if req.FAQID > 0 {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeFAQUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := rag.Index.IndexFAQByID(context.Background(), req.FAQID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
return
|
||||
}
|
||||
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0066"))
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func KnowledgeRetrieveLogAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "knowledgeBaseId"},
|
||||
params.QueryFilter{ParamName: "question", Op: params.Like},
|
||||
params.QueryFilter{ParamName: "channel"},
|
||||
params.QueryFilter{ParamName: "scene"},
|
||||
params.QueryFilter{ParamName: "chunkProvider"},
|
||||
).Desc("id")
|
||||
|
||||
if answerStatus, ok := params.GetInt64(ctx, "answerStatus"); ok && answerStatus > 0 {
|
||||
cnd.Where("answer_status = ?", answerStatus)
|
||||
}
|
||||
if rerankEnabled, ok := params.GetInt64(ctx, "rerankEnabled"); ok {
|
||||
cnd.Where("rerank_enabled = ?", rerankEnabled > 0)
|
||||
}
|
||||
|
||||
queryParams := params.NewQueryParams(ctx)
|
||||
queryParams.Cnd = *cnd
|
||||
list, paging := services.KnowledgeRetrieveLogService.FindPageByParams(queryParams)
|
||||
results := make([]response.KnowledgeRetrieveLogResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
resp := builders.BuildKnowledgeRetrieveLog(&item)
|
||||
if knowledgeBase := services.KnowledgeBaseService.Get(item.KnowledgeBaseID); knowledgeBase != nil {
|
||||
resp.KnowledgeBaseName = knowledgeBase.Name
|
||||
}
|
||||
results = append(results, resp)
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func KnowledgeRetrieveLogGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionKnowledgeDocumentView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
logItem := services.KnowledgeRetrieveLogService.Get(id)
|
||||
if logItem == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0241"))
|
||||
return
|
||||
}
|
||||
|
||||
logResp := builders.BuildKnowledgeRetrieveLog(logItem)
|
||||
if knowledgeBase := services.KnowledgeBaseService.Get(logItem.KnowledgeBaseID); knowledgeBase != nil {
|
||||
logResp.KnowledgeBaseName = knowledgeBase.Name
|
||||
}
|
||||
|
||||
hits := services.KnowledgeRetrieveLogService.FindHitsByRetrieveLogID(id)
|
||||
hitResults := make([]response.KnowledgeRetrieveHitResponse, 0, len(hits))
|
||||
for _, item := range hits {
|
||||
hitResults = append(hitResults, builders.BuildKnowledgeRetrieveHitResponse(&item))
|
||||
}
|
||||
|
||||
httpx.WriteJSON(ctx, response.KnowledgeRetrieveLogDetailResponse{
|
||||
Log: logResp,
|
||||
Hits: hitResults,
|
||||
})
|
||||
}
|
||||
108
internal/handlers/dashboard/mcp_handler.go
Normal file
108
internal/handlers/dashboard/mcp_handler.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"context"
|
||||
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func MCPAnyList_servers(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionMCPView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, response.BuildMCPServerInfoResponses(services.MCPDebugService.ListServers()))
|
||||
}
|
||||
|
||||
func MCPAnyCatalog(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionMCPView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
items, err := services.ToolCatalogService.ListMCPToolsWithLocale(context.Background(), i18nx.Locale(ctx))
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
ret := make([]response.MCPToolCatalogResponse, 0, len(items))
|
||||
for _, item := range items {
|
||||
ret = append(ret, response.MCPToolCatalogResponse{
|
||||
ToolCode: item.ToolCode,
|
||||
ServerCode: item.ServerCode,
|
||||
ToolName: item.ToolName,
|
||||
SourceType: item.SourceType,
|
||||
AutoInjected: item.AutoInjected,
|
||||
Title: item.Title,
|
||||
Description: item.Description,
|
||||
InputSchema: item.InputSchema,
|
||||
OutputSchema: item.OutputSchema,
|
||||
RiskLevel: item.RiskLevel,
|
||||
RequireConfirmation: item.RequireConfirmation,
|
||||
RiskEditable: item.RiskEditable,
|
||||
})
|
||||
}
|
||||
httpx.WriteJSON(ctx, ret)
|
||||
}
|
||||
|
||||
func MCPPostTest_connection(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionMCPView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.MCPServerDebugRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
result, err := services.MCPDebugService.TestConnection(context.Background(), req.ServerCode)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, response.BuildMCPConnectionResponse(result))
|
||||
}
|
||||
|
||||
func MCPPostList_tools(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionMCPView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.MCPServerDebugRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
result, err := services.MCPDebugService.ListTools(context.Background(), req.ServerCode)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, response.BuildMCPToolInfoResponses(result))
|
||||
}
|
||||
|
||||
func MCPPostCall_tool(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionMCPCall); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.MCPCallToolRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
result, err := services.MCPDebugService.CallTool(context.Background(), req.ServerCode, req.ToolName, req.Arguments)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, response.BuildMCPCallToolResponse(result))
|
||||
}
|
||||
88
internal/handlers/dashboard/notification_handler.go
Normal file
88
internal/handlers/dashboard/notification_handler.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func NotificationAnyList(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionNotificationView)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "type", ColumnName: "notification_type"},
|
||||
).Eq("recipient_user_id", operator.UserID).
|
||||
Eq("status", enums.StatusOk).
|
||||
Desc("id")
|
||||
|
||||
switch strings.TrimSpace(ctx.Query("readStatus")) {
|
||||
case "unread":
|
||||
cnd.Where("read_at IS NULL")
|
||||
case "read":
|
||||
cnd.Where("read_at IS NOT NULL")
|
||||
}
|
||||
|
||||
list, paging := services.NotificationService.FindPageByCnd(cnd)
|
||||
httpx.WriteJSON(ctx, &web.PageResult{
|
||||
Results: builders.BuildNotificationListWithLocale(list, i18nx.Locale(ctx)),
|
||||
Page: paging,
|
||||
})
|
||||
}
|
||||
|
||||
func NotificationGetUnread_count(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionNotificationView)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, &response.NotificationUnreadCountResponse{
|
||||
UnreadCount: services.NotificationService.CountUnread(operator.UserID),
|
||||
})
|
||||
}
|
||||
|
||||
func NotificationPostMark_read(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionNotificationUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.MarkNotificationReadRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.NotificationService.MarkRead(req.ID, operator.UserID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func NotificationPostMark_all_read(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionNotificationUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.NotificationService.MarkAllRead(operator.UserID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
47
internal/handlers/dashboard/organization_handler.go
Normal file
47
internal/handlers/dashboard/organization_handler.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func OrganizationUserList(ctx *gin.Context) {
|
||||
principal := services.AuthService.GetAuthPrincipal(ctx)
|
||||
if principal == nil {
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ret, err := services.OrganizationService.GetUserOrganizations(principal.UserID)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, ret)
|
||||
}
|
||||
|
||||
func OrganizationSwitch(ctx *gin.Context) {
|
||||
principal := services.AuthService.GetAuthPrincipal(ctx)
|
||||
if principal == nil {
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.OrganizationSwitchRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
org, err := services.OrganizationService.SwitchActiveOrganization(principal.UserID, req.OrganizationID)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpx.WriteJSON(ctx, org)
|
||||
}
|
||||
89
internal/handlers/dashboard/permission_handler.go
Normal file
89
internal/handlers/dashboard/permission_handler.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/common/strs"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func PermissionAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionPermissionView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "groupName"},
|
||||
params.QueryFilter{ParamName: "type"},
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
).Desc("id")
|
||||
|
||||
if keyword, _ := params.Get(ctx, "keyword"); strs.IsNotBlank(keyword) {
|
||||
cnd.Where("(name LIKE ? OR code LIKE ?)", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
list, paging := services.PermissionService.FindPageByCnd(cnd)
|
||||
results := make([]response.PermissionResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, response.PermissionResponse{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Code: item.Code,
|
||||
Type: item.Type,
|
||||
GroupName: item.GroupName,
|
||||
Method: item.Method,
|
||||
ApiPath: item.APIPath,
|
||||
Status: item.Status,
|
||||
SortNo: item.SortNo,
|
||||
})
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func PermissionGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionPermissionView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := services.PermissionService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0236"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, &response.PermissionResponse{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Code: item.Code,
|
||||
Type: item.Type,
|
||||
GroupName: item.GroupName,
|
||||
Method: item.Method,
|
||||
ApiPath: item.APIPath,
|
||||
Status: item.Status,
|
||||
SortNo: item.SortNo,
|
||||
})
|
||||
}
|
||||
|
||||
func PermissionPostSync(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionPermissionSync); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
result, err := services.PermissionService.SyncBuiltinPermissions()
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, result)
|
||||
}
|
||||
118
internal/handlers/dashboard/quick_reply_handler.go
Normal file
118
internal/handlers/dashboard/quick_reply_handler.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func QuickReplyAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionQuickReplyView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "groupName"},
|
||||
params.QueryFilter{ParamName: "title", Op: params.Like},
|
||||
).Asc("sort_no").Desc("id")
|
||||
|
||||
list, paging := services.QuickReplyService.FindPageByCnd(cnd)
|
||||
results := builders.BuildQuickReplyResponses(list)
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func QuickReplyGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionQuickReplyView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := services.QuickReplyService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0203"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildQuickReplyResponse(item))
|
||||
}
|
||||
|
||||
func QuickReplyGetList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionQuickReplyView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list := services.QuickReplyService.Find(sqls.NewCnd().Eq("status", enums.StatusOk).Asc("sort_no").Desc("id"))
|
||||
results := builders.BuildQuickReplyResponses(list)
|
||||
httpx.WriteJSON(ctx, results)
|
||||
}
|
||||
|
||||
func QuickReplyPostCreate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionQuickReplyCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.CreateQuickReplyRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.QuickReplyService.CreateQuickReply(req, user)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildQuickReplyResponse(item))
|
||||
}
|
||||
|
||||
func QuickReplyPostUpdate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionQuickReplyUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateQuickReplyRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.QuickReplyService.UpdateQuickReply(req, user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func QuickReplyPostDelete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionQuickReplyDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.DeleteQuickReplyRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.QuickReplyService.DeleteQuickReply(req.ID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
210
internal/handlers/dashboard/role_handler.go
Normal file
210
internal/handlers/dashboard/role_handler.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func RoleAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionRoleView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "code", Op: params.Like},
|
||||
).Asc("sort_no").Desc("id")
|
||||
list, paging := services.RoleService.FindPageByCnd(cnd)
|
||||
results := make([]response.RoleResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, response.RoleResponse{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Code: item.Code,
|
||||
Status: item.Status,
|
||||
IsSystem: item.IsSystem,
|
||||
SortNo: item.SortNo,
|
||||
})
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func RoleGetList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionRoleView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
list := services.RoleService.Find(sqls.NewCnd().Asc("sort_no").Desc("id"))
|
||||
results := make([]response.RoleResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, response.RoleResponse{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Code: item.Code,
|
||||
Status: item.Status,
|
||||
IsSystem: item.IsSystem,
|
||||
SortNo: item.SortNo,
|
||||
})
|
||||
}
|
||||
httpx.WriteJSON(ctx, results)
|
||||
}
|
||||
|
||||
func RoleGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionRoleView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := services.RoleService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0305"))
|
||||
return
|
||||
}
|
||||
|
||||
permissionCodes := make([]string, 0)
|
||||
list := services.RolePermissionService.Find(sqls.NewCnd().Eq("role_id", item.ID))
|
||||
for _, relation := range list {
|
||||
permission := services.PermissionService.Get(relation.PermissionID)
|
||||
if permission != nil {
|
||||
permissionCodes = append(permissionCodes, permission.Code)
|
||||
}
|
||||
}
|
||||
httpx.WriteJSON(ctx, &response.RoleResponse{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Code: item.Code,
|
||||
Status: item.Status,
|
||||
IsSystem: item.IsSystem,
|
||||
SortNo: item.SortNo,
|
||||
Permissions: permissionCodes,
|
||||
})
|
||||
}
|
||||
|
||||
func RolePostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionRoleCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.CreateRoleRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
role, err := services.RoleService.CreateRole(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, &response.RoleResponse{
|
||||
ID: role.ID,
|
||||
Name: role.Name,
|
||||
Code: role.Code,
|
||||
Status: role.Status,
|
||||
IsSystem: role.IsSystem,
|
||||
SortNo: role.SortNo,
|
||||
})
|
||||
}
|
||||
|
||||
func RolePostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionRoleUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateRoleRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.RoleService.UpdateRole(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func RolePostDelete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionRoleDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.DeleteRoleRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.RoleService.DeleteRole(req.ID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func RolePostUpdate_status(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionRoleUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateRoleStatusRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.RoleService.UpdateStatus(req.ID, req.Status, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func RolePostAssign_permission(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionRoleAssignPermission)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.AssignPermissionRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.RoleService.AssignPermissions(req.RoleID, req.PermissionIDs, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func RolePostUpdate_sort(ctx *gin.Context) {
|
||||
var ids []int64
|
||||
if err := params.ReadJSON(ctx, &ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.RoleService.UpdateSort(ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
86
internal/handlers/dashboard/session_handler.go
Normal file
86
internal/handlers/dashboard/session_handler.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/utils"
|
||||
"agent-desk/internal/services"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func SessionAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionSessionView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "userId"},
|
||||
params.QueryFilter{ParamName: "clientType"},
|
||||
).Desc("id")
|
||||
list, paging := services.LoginSessionService.FindPageByCnd(cnd)
|
||||
results := make([]response.SessionResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
username := ""
|
||||
if user := services.UserService.Get(item.UserID); user != nil {
|
||||
username = user.Username
|
||||
}
|
||||
results = append(results, response.SessionResponse{
|
||||
ID: item.ID,
|
||||
UserID: item.UserID,
|
||||
Username: username,
|
||||
ClientType: item.ClientType,
|
||||
ClientIP: item.ClientIP,
|
||||
UserAgent: item.UserAgent,
|
||||
ExpiredAt: item.ExpiredAt.Format(time.DateTime),
|
||||
RevokedAt: utils.FormatTimePtr(item.RevokedAt),
|
||||
LastSeenAt: utils.FormatTimePtr(item.LastSeenAt),
|
||||
})
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func SessionPostRevoke(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionSessionRevoke)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.RevokeSessionRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.LoginSessionService.Revoke(req.ID, user.UserID, user.Username); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func SessionPostRevokeByUser(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionSessionRevoke)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.RevokeUserSessionsRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.LoginSessionService.RevokeByUser(req.UserID, user.UserID, user.Username); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
64
internal/handlers/dashboard/setup_readiness_handler.go
Normal file
64
internal/handlers/dashboard/setup_readiness_handler.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupGetReadiness(ctx *gin.Context) {
|
||||
principal, err := services.AuthService.Authenticate(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
res, err := services.SetupReadinessService.GetReadiness(principal)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, res)
|
||||
}
|
||||
|
||||
func SetupPostSimulate_route(ctx *gin.Context) {
|
||||
principal, err := services.AuthService.Authenticate(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.SetupSimulateRouteRequest{}
|
||||
if ctx.Request != nil && ctx.Request.ContentLength > 0 {
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
res, err := services.SetupReadinessService.SimulateRoute(req, principal)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, res)
|
||||
}
|
||||
|
||||
func SetupPostQuick_fix(ctx *gin.Context) {
|
||||
principal, err := services.AuthService.Authenticate(ctx)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.SetupQuickFixRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
res, err := services.SetupReadinessService.ApplyQuickFix(req, principal)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, res)
|
||||
}
|
||||
273
internal/handlers/dashboard/skill_definition_handler.go
Normal file
273
internal/handlers/dashboard/skill_definition_handler.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func SkillDefinitionAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionSkillDefinitionView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "name", Op: params.Like},
|
||||
).Desc("id")
|
||||
if _, ok := params.Get(ctx, "status"); !ok {
|
||||
cnd.Where("status <> ?", enums.StatusDeleted)
|
||||
}
|
||||
list, paging := services.SkillDefinitionService.FindPageByCnd(cnd)
|
||||
results := make([]response.SkillDefinitionResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, builders.BuildSkillDefinitionResponse(&item))
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func SkillDefinitionGetList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionSkillDefinitionView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
).Desc("id")
|
||||
if status, ok := params.Get(ctx, "status"); !ok || strings.TrimSpace(status) == "" {
|
||||
cnd.Where("status <> ?", enums.StatusDeleted)
|
||||
}
|
||||
list := services.SkillDefinitionService.Find(cnd)
|
||||
results := make([]response.SkillDefinitionResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
results = append(results, builders.BuildSkillDefinitionResponse(&item))
|
||||
}
|
||||
httpx.WriteJSON(ctx, results)
|
||||
}
|
||||
|
||||
func SkillDefinitionGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionSkillDefinitionView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := services.SkillDefinitionService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0053"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildSkillDefinitionResponse(item))
|
||||
}
|
||||
|
||||
func SkillDefinitionPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionSkillDefinitionCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.CreateSkillDefinitionRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.SkillDefinitionService.CreateSkillDefinition(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildSkillDefinitionResponse(item))
|
||||
}
|
||||
|
||||
func SkillDefinitionPostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionSkillDefinitionUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateSkillDefinitionRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.SkillDefinitionService.UpdateSkillDefinition(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func SkillDefinitionPostUpdate_status(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionSkillDefinitionUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateSkillDefinitionStatusRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if req.ID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0052"))
|
||||
return
|
||||
}
|
||||
if !enums.IsValidStatus(req.Status) {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0254"))
|
||||
return
|
||||
}
|
||||
item := services.SkillDefinitionService.Get(req.ID)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0053"))
|
||||
return
|
||||
}
|
||||
if item.Status == enums.StatusDeleted {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0184"))
|
||||
return
|
||||
}
|
||||
if req.Status == int(enums.StatusDeleted) {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0319"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.SkillDefinitionService.Updates(req.ID, map[string]any{
|
||||
"status": req.Status,
|
||||
"update_user_id": operator.UserID,
|
||||
"update_user_name": operator.Username,
|
||||
"updated_at": time.Now(),
|
||||
}); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func SkillDefinitionPostDelete(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionSkillDefinitionDelete)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.DeleteSkillDefinitionRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if req.ID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0052"))
|
||||
return
|
||||
}
|
||||
if services.SkillDefinitionService.Get(req.ID) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0053"))
|
||||
return
|
||||
}
|
||||
if err := services.SkillDefinitionService.Updates(req.ID, map[string]any{
|
||||
"status": enums.StatusDeleted,
|
||||
"update_user_id": operator.UserID,
|
||||
"update_user_name": operator.Username,
|
||||
"updated_at": time.Now(),
|
||||
}); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func SkillDefinitionPostRestore(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionSkillDefinitionDelete)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.RestoreSkillDefinitionRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if req.ID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0052"))
|
||||
return
|
||||
}
|
||||
|
||||
item := services.SkillDefinitionService.Get(req.ID)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0053"))
|
||||
return
|
||||
}
|
||||
if item.Status != enums.StatusDeleted {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0088"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.SkillDefinitionService.Updates(req.ID, map[string]any{
|
||||
"status": enums.StatusDisabled,
|
||||
"update_user_id": operator.UserID,
|
||||
"update_user_name": operator.Username,
|
||||
"updated_at": time.Now(),
|
||||
}); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func SkillDefinitionPostDebug_run(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionSkillDefinitionView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.SkillDebugRunRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := services.SkillRuntimeService.DebugRun(context.Background(), req)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, resp)
|
||||
}
|
||||
|
||||
func SkillDefinitionPostDebug_resume(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionSkillDefinitionView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.SkillDebugResumeRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := services.SkillRuntimeService.DebugResume(context.Background(), req)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, resp)
|
||||
}
|
||||
49
internal/handlers/dashboard/support_config_handler.go
Normal file
49
internal/handlers/dashboard/support_config_handler.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func SupportConfigGetConfig(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionSupportConfigView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SystemConfigService.GetDashboardSupportConfig())
|
||||
}
|
||||
|
||||
func SupportConfigPostSave(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionSupportConfigUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := map[string]json.RawMessage{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
config, err := services.SystemConfigService.SaveSupportConfig(req, operator)
|
||||
if err != nil {
|
||||
if validationErr, ok := err.(*services.SystemConfigValidationError); ok {
|
||||
locale := i18nx.Locale(ctx)
|
||||
httpx.WriteJSON(ctx, web.JsonErrorData(errorsx.CodeInvalidParam, validationErr.Message(locale), gin.H{
|
||||
"errors": validationErr.FieldErrorsLocale(locale),
|
||||
}))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, config)
|
||||
}
|
||||
375
internal/handlers/dashboard/support_handler.go
Normal file
375
internal/handlers/dashboard/support_handler.go
Normal file
@@ -0,0 +1,375 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/repositories"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func DocPageAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionDocPageView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list, paging := services.SupportService.FindDocPagePage(params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "parentId"},
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "title", Op: params.Like},
|
||||
).Asc("sort_no").Desc("id"))
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: buildDashboardDocPages(list, false), Page: paging})
|
||||
}
|
||||
|
||||
func DocPageGetList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionDocPageView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list := services.SupportService.FindDocPages(sqls.NewCnd().Asc("sort_no").Asc("id"))
|
||||
httpx.WriteJSON(ctx, buildDashboardDocPages(list, false))
|
||||
}
|
||||
|
||||
func DocPageGetBy(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionDocPageView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
item := services.SupportService.FindDocPageByID(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.notFound"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildDocPage(item, true))
|
||||
}
|
||||
|
||||
func DocPagePostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionDocPageCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.SaveDocPageRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.SupportService.SaveDocPage(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildDocPage(item, true))
|
||||
}
|
||||
|
||||
func DocPagePostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionDocPageUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.SaveDocPageRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.SupportService.SaveDocPage(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildDocPage(item, true))
|
||||
}
|
||||
|
||||
func DocPagePostUpdate_settings(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionDocPageUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateDocPageSettingsRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.SupportService.UpdateDocPageSettings(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildDocPage(item, true))
|
||||
}
|
||||
|
||||
func DocPagePostDelete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionDocPageDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteByIDRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.DeleteDocPage(req.ID))
|
||||
}
|
||||
|
||||
func DocPagePostUpdate_sort(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionDocPageUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.SortDocPagesRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.SortDocPages(req))
|
||||
}
|
||||
|
||||
func DocPagePostChange_status(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionDocPageUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.ChangeDocPageStatusRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.SupportService.ChangeDocPageStatus(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildDocPage(item, true))
|
||||
}
|
||||
|
||||
func CategoryAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCommunityView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list, paging := repositories.CategoryRepository.FindPageByCnd(sqls.DB(), params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "name", Op: params.Like},
|
||||
).Asc("sort_no").Desc("id"))
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: builders.BuildPostCategories(list), Page: paging})
|
||||
}
|
||||
|
||||
func CategoryGetList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCommunityView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list := repositories.CategoryRepository.Find(sqls.DB(), sqls.NewCnd().Asc("sort_no").Desc("id"))
|
||||
httpx.WriteJSON(ctx, builders.BuildPostCategories(list))
|
||||
}
|
||||
|
||||
func CategoryPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionCommunityUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.SaveCategoryRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.SupportService.SaveCategory(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildCategory(item))
|
||||
}
|
||||
|
||||
func CategoryPostUpdate(ctx *gin.Context) {
|
||||
CategoryPostCreate(ctx)
|
||||
}
|
||||
|
||||
func CategoryPostUpdateSort(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCommunityUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
var ids []int64
|
||||
if err := params.ReadJSON(ctx, &ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.SupportService.UpdateCategorySort(ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func CategoryPostDelete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCommunityUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteByIDRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.DeleteCategory(req.ID))
|
||||
}
|
||||
|
||||
func PostAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCommunityView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list, paging := repositories.PostRepository.FindPageByCnd(sqls.DB(), params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "categoryId"},
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "title", Op: params.Like},
|
||||
).Desc("id"))
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: buildDashboardPosts(list), Page: paging})
|
||||
}
|
||||
|
||||
func PostGetBy(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCommunityView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
post := repositories.PostRepository.Get(sqls.DB(), id)
|
||||
if post == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.notFound"))
|
||||
return
|
||||
}
|
||||
comments := repositories.CommentRepository.Find(sqls.DB(), sqls.NewCnd().Eq("post_id", id).Desc("is_accepted").Asc("id"))
|
||||
httpx.WriteJSON(ctx, response.PostDetailResponse{Post: *builders.BuildPost(post, dashboardCategoryName(post.CategoryID), dashboardSupportUser(post.UserID)), Comments: buildDashboardComments(comments)})
|
||||
}
|
||||
|
||||
func PostPostModerate(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCommunityUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.ModeratePostRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.ModeratePost(req))
|
||||
}
|
||||
|
||||
func PostPostAcceptComment(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionCommunityUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.AcceptCommentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.AcceptComment(req, nil, operator))
|
||||
}
|
||||
|
||||
func CommentPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionCommunityUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateCommentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.SupportService.CreateUserComment(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildComment(item, operator.Nickname))
|
||||
}
|
||||
|
||||
func CommentPostModerate(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionCommunityUpdate); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.ModerateCommentRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, services.SupportService.ModerateComment(req))
|
||||
}
|
||||
|
||||
func buildDashboardDocPages(list []models.DocPage, includeContent bool) []response.DocPageResponse {
|
||||
results := make([]response.DocPageResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
if resp := builders.BuildDocPage(&item, includeContent); resp != nil {
|
||||
results = append(results, *resp)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func buildDashboardPosts(list []models.Post) []response.PostResponse {
|
||||
results := make([]response.PostResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
if resp := builders.BuildPost(&item, dashboardCategoryName(item.CategoryID), dashboardSupportUser(item.UserID)); resp != nil {
|
||||
results = append(results, *resp)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func buildDashboardComments(list []models.Comment) []response.CommentResponse {
|
||||
results := make([]response.CommentResponse, 0, len(list))
|
||||
for _, item := range list {
|
||||
if resp := builders.BuildComment(&item, dashboardCommentAuthorName(item)); resp != nil {
|
||||
results = append(results, *resp)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func dashboardCategoryName(id int64) string {
|
||||
item := repositories.CategoryRepository.Get(sqls.DB(), id)
|
||||
if item == nil {
|
||||
return ""
|
||||
}
|
||||
return item.Name
|
||||
}
|
||||
|
||||
func dashboardSupportUser(id int64) *models.User {
|
||||
return repositories.UserRepository.Get(sqls.DB(), id)
|
||||
}
|
||||
|
||||
func dashboardCommentAuthorName(item models.Comment) string {
|
||||
user := repositories.UserRepository.Get(sqls.DB(), item.AuthorID)
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
if user.Nickname != "" {
|
||||
return user.Nickname
|
||||
}
|
||||
return user.Username
|
||||
}
|
||||
147
internal/handlers/dashboard/tag_handler.go
Normal file
147
internal/handlers/dashboard/tag_handler.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func TagAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionTagView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list, paging := services.TagService.FindPageByCnd(params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "parentId"},
|
||||
params.QueryFilter{ParamName: "name", Op: params.Like},
|
||||
).Asc("sort_no").Desc("id"))
|
||||
results := builders.BuildTagResponses(list)
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func TagGetList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionTagView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
list := services.TagService.FindAll()
|
||||
results := builders.BuildTagTreeResponses(list)
|
||||
httpx.WriteJSON(ctx, results)
|
||||
}
|
||||
|
||||
func TagGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionTagView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := services.TagService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0238"))
|
||||
return
|
||||
}
|
||||
result := builders.BuildTagResponse(item)
|
||||
httpx.WriteJSON(ctx, &result)
|
||||
}
|
||||
|
||||
func TagPostCreate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionTagCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.CreateTagRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.TagService.CreateTag(req, user)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
result := builders.BuildTagResponse(item)
|
||||
httpx.WriteJSON(ctx, &result)
|
||||
}
|
||||
|
||||
func TagPostUpdate(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionTagUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateTagRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.TagService.UpdateTag(req, user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func TagPostDelete(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionTagDelete); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.DeleteTagRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.TagService.DeleteTag(req.ID); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func TagPostUpdate_sort(ctx *gin.Context) {
|
||||
var ids []int64
|
||||
if err := params.ReadJSON(ctx, &ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.TagService.UpdateSort(ids); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func TagPostUpdate_status(ctx *gin.Context) {
|
||||
user, err := services.AuthService.RequirePermission(ctx, constants.PermissionTagUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateTagStatusRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.TagService.UpdateStatus(req.ID, req.Status, user); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
287
internal/handlers/dashboard/ticket_handler.go
Normal file
287
internal/handlers/dashboard/ticket_handler.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
func TicketAnyList(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketView)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "currentAssigneeId"},
|
||||
params.QueryFilter{ParamName: "customerId"},
|
||||
params.QueryFilter{ParamName: "conversationId"},
|
||||
params.QueryFilter{ParamName: "source"},
|
||||
params.QueryFilter{ParamName: "channel"},
|
||||
).Desc("updated_at").Desc("id")
|
||||
if keyword, _ := params.Get(ctx, "keyword"); strings.TrimSpace(keyword) != "" {
|
||||
keyword = "%" + strings.TrimSpace(keyword) + "%"
|
||||
cnd.Where("ticket_no LIKE ? OR title LIKE ? OR description LIKE ?", keyword, keyword, keyword)
|
||||
}
|
||||
if tagID, _ := params.GetInt64(ctx, "tagId"); tagID > 0 {
|
||||
cnd.Where("id IN (SELECT ticket_id FROM t_ticket_tag WHERE tag_id = ?)", tagID)
|
||||
}
|
||||
if mine, _ := params.Get(ctx, "mine"); mine == "1" || strings.EqualFold(mine, "true") {
|
||||
cnd.Eq("current_assignee_id", operator.UserID)
|
||||
}
|
||||
if unassigned, _ := params.Get(ctx, "unassigned"); unassigned == "1" || strings.EqualFold(unassigned, "true") {
|
||||
cnd.Eq("current_assignee_id", 0)
|
||||
}
|
||||
if staleHoursValue, _ := params.Get(ctx, "staleHours"); strings.TrimSpace(staleHoursValue) != "" {
|
||||
staleHours, _ := params.GetInt(ctx, "staleHours")
|
||||
services.TicketService.ApplyStaleFilter(cnd, staleHours)
|
||||
}
|
||||
aggregate, err := services.TicketService.FindPageAggregateByCnd(cnd, operator.UserID)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, &web.PageResult{
|
||||
Results: builders.BuildTicketListWithContext(aggregate.List, &builders.TicketBuildContext{
|
||||
TagsByTicketID: aggregate.TagsByTicketID,
|
||||
Users: aggregate.Users,
|
||||
Customers: aggregate.Customers,
|
||||
}),
|
||||
Page: aggregate.Paging,
|
||||
})
|
||||
}
|
||||
|
||||
func TicketAnySummary(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketView)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
staleHours, _ := params.GetInt(ctx, "staleHours")
|
||||
httpx.WriteJSON(ctx, builders.BuildTicketSummary(services.TicketService.GetSummary(operator, staleHours)))
|
||||
}
|
||||
|
||||
func TicketAnyView_list(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketView)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildTicketViewList(services.TicketViewService.ListByUser(operator.UserID)))
|
||||
}
|
||||
|
||||
func TicketPostSave_view(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketView)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.SaveTicketViewRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.TicketViewService.Save(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildTicketView(item))
|
||||
}
|
||||
|
||||
func TicketPostDelete_view(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketView)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.DeleteTicketViewRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.TicketViewService.Delete(req.ID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func TicketGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
detail, err := services.TicketService.GetDetail(id)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildTicketDetail(detail))
|
||||
}
|
||||
|
||||
func TicketPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateTicketRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.TicketService.CreateTicket(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildTicket(item))
|
||||
}
|
||||
|
||||
func TicketPostCreate_from_conversation(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateTicketFromConversationRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.TicketService.CreateFromConversation(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildTicket(item))
|
||||
}
|
||||
|
||||
func TicketPostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.UpdateTicketRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.TicketService.UpdateTicket(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func TicketPostLink_customer(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.LinkTicketCustomerRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.TicketService.LinkCustomer(req.TicketID, req.CustomerID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func TicketPostAssign(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketAssign)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.AssignTicketRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.TicketService.AssignTicket(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func TicketPostChange_status(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketChangeStatus)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.ChangeTicketStatusRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.TicketService.ChangeStatus(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func TicketAnyProgressList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
ticketID, _ := params.GetInt64(ctx, "ticketId")
|
||||
if ticketID <= 0 {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0178"))
|
||||
return
|
||||
}
|
||||
if services.TicketService.Get(ticketID) == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0178"))
|
||||
return
|
||||
}
|
||||
progresses := services.TicketProgressService.Find(sqls.NewCnd().Eq("ticket_id", ticketID).Asc("id"))
|
||||
httpx.WriteJSON(ctx, builders.BuildTicketProgressList(progresses))
|
||||
}
|
||||
|
||||
func TicketPostProgressCreate(ctx *gin.Context) {
|
||||
ticketCreateProgress(ctx)
|
||||
}
|
||||
|
||||
func ticketCreateProgress(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionTicketProgress)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
req := request.CreateTicketProgressRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
item, err := services.TicketService.AddProgress(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildTicketProgress(item))
|
||||
}
|
||||
234
internal/handlers/dashboard/user_handler.go
Normal file
234
internal/handlers/dashboard/user_handler.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"agent-desk/internal/builders"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/dto/response"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/web"
|
||||
)
|
||||
|
||||
// Portal visitors live in the same table as staff - UserType is what separates
|
||||
// them - and without a filter on it they sat in the middle of the agent list
|
||||
// with admin actions offered on them. Neither group is deleted or hidden; they
|
||||
// are just no longer indistinguishable. user_type is a varchar, so the filter is
|
||||
// a plain equality and an unknown value simply matches nothing.
|
||||
func UserAnyList(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionUserView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewPagedSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "username", Op: params.Like},
|
||||
params.QueryFilter{ParamName: "nickname", Op: params.Like},
|
||||
params.QueryFilter{ParamName: "userType"},
|
||||
).Desc("id")
|
||||
cnd.Where("status <> ?", enums.StatusDeleted)
|
||||
list, paging := services.UserService.FindPageByCnd(cnd)
|
||||
results := builders.BuildUserList(list, builders.UserBuildOptions{
|
||||
Roles: true,
|
||||
Permissions: false,
|
||||
})
|
||||
httpx.WriteJSON(ctx, &web.PageResult{Results: results, Page: paging})
|
||||
}
|
||||
|
||||
func UserAnyList_all(ctx *gin.Context) {
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionUserView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
cnd := params.NewSqlCnd(ctx,
|
||||
params.QueryFilter{ParamName: "status"},
|
||||
params.QueryFilter{ParamName: "username", Op: params.Like},
|
||||
params.QueryFilter{ParamName: "nickname", Op: params.Like},
|
||||
params.QueryFilter{ParamName: "userType"},
|
||||
).Desc("id")
|
||||
cnd.Where("status <> ?", enums.StatusDeleted)
|
||||
|
||||
list := services.UserService.Find(cnd)
|
||||
results := builders.BuildUserList(list, builders.UserBuildOptions{
|
||||
Roles: true,
|
||||
Permissions: false,
|
||||
})
|
||||
httpx.WriteJSON(ctx, results)
|
||||
}
|
||||
|
||||
func UserGetBy(ctx *gin.Context) {
|
||||
id, ok := httpx.GetPathInt64(ctx, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := services.AuthService.RequirePermission(ctx, constants.PermissionUserView); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
item := services.UserService.Get(id)
|
||||
if item == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.e0255"))
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, builders.BuildUserResponse(item, builders.UserBuildOptions{
|
||||
Roles: true,
|
||||
Permissions: true,
|
||||
}))
|
||||
}
|
||||
|
||||
func UserPostCreate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionUserCreate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.CreateUserRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
user, generatedPassword, err := services.UserService.CreateUser(req, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, &response.CreateUserResultResponse{
|
||||
User: builders.BuildUserResponse(user, builders.UserBuildOptions{Roles: true, Permissions: true}),
|
||||
Password: generatedPassword,
|
||||
})
|
||||
}
|
||||
|
||||
func UserPostUpdate(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionUserUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateUserRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.UserService.UpdateUser(req, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func UserPostDelete(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionUserDelete)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.DeleteUserRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.UserService.DeleteUser(req.ID, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func UserPostUpdate_status(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionUserUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.UpdateUserStatusRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.UserService.UpdateStatus(req.ID, req.Status, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func UserPostReset_password(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionUserUpdate)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID int64 `json:"userId"`
|
||||
}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
password, err := services.UserService.ResetPassword(req.UserID, operator)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, map[string]any{
|
||||
"password": password,
|
||||
})
|
||||
}
|
||||
|
||||
func UserPostChange_password(ctx *gin.Context) {
|
||||
principal := services.AuthService.GetAuthPrincipal(ctx)
|
||||
if principal == nil {
|
||||
if _, err := services.AuthService.Authenticate(ctx); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
principal = services.AuthService.GetAuthPrincipal(ctx)
|
||||
}
|
||||
if principal == nil {
|
||||
httpx.WriteJSON(ctx, httpx.JsonErrorMsg(ctx, "error.auth.expired"))
|
||||
return
|
||||
}
|
||||
|
||||
req := request.ChangePasswordRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.UserService.ChangeOwnPassword(req.Password, principal); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
|
||||
func UserPostAssign_role(ctx *gin.Context) {
|
||||
operator, err := services.AuthService.RequirePermission(ctx, constants.PermissionUserAssignRole)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := request.AssignRoleRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := services.UserService.AssignRoles(req.UserID, req.RoleIDs, operator); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, nil)
|
||||
}
|
||||
39
internal/handlers/third/discord_handler.go
Normal file
39
internal/handlers/third/discord_handler.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package third
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DiscordPostWebhook receives incoming Webhook events from Discord.
|
||||
func DiscordPostWebhook(ctx *gin.Context) {
|
||||
channelID := strings.TrimSpace(ctx.Param("channel_id"))
|
||||
if channelID == "" {
|
||||
channelID = strings.TrimSpace(ctx.Query("channel_id"))
|
||||
}
|
||||
|
||||
secretHeader := ctx.GetHeader("X-Discord-Secret-Token")
|
||||
if secretHeader == "" {
|
||||
secretHeader = ctx.GetHeader("X-Webhook-Secret")
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(ctx.Request.Body)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "failed to read body"})
|
||||
return
|
||||
}
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
if err := services.DiscordInboundService.HandleWebhook(ctx.Request.Context(), channelID, secretHeader, bodyBytes); err != nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
115
internal/handlers/third/discord_handler_test.go
Normal file
115
internal/handlers/third/discord_handler_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package third
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/repositories"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
func TestDiscordPostWebhook_Handler(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupThirdHandlerTestDB(t)
|
||||
|
||||
now := time.Now()
|
||||
agent := &models.AIAgent{
|
||||
Name: "Discord Agent",
|
||||
ServiceMode: enums.IMConversationServiceModeAIFirst,
|
||||
PublishedRevisionID: 1,
|
||||
WelcomeMessage: "Hello Discord!",
|
||||
Status: enums.StatusOk,
|
||||
AuditFields: models.AuditFields{CreatedAt: now, UpdatedAt: now},
|
||||
}
|
||||
_ = db.Create(agent)
|
||||
|
||||
discordConfig, _ := json.Marshal(dto.DiscordChannelConfig{
|
||||
GuildID: "guild_999",
|
||||
BotToken: "test_bot_token",
|
||||
WebhookSecret: "secret_discord_123",
|
||||
WelcomeMessage: "Welcome!",
|
||||
})
|
||||
|
||||
operator := &dto.AuthPrincipal{UserID: 1, Username: "admin"}
|
||||
channel, err := services.ChannelService.CreateChannel(request.CreateChannelRequest{
|
||||
Name: "Discord Community",
|
||||
ChannelType: enums.ChannelTypeDiscord,
|
||||
AIAgentID: agent.ID,
|
||||
AIAgentRolloutPercent: 100,
|
||||
ConfigJSON: string(discordConfig),
|
||||
Status: int(enums.StatusOk),
|
||||
}, operator)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateChannel failed: %v", err)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/third/discord/webhook/:channel_id", DiscordPostWebhook)
|
||||
router.POST("/api/third/discord/webhook", DiscordPostWebhook)
|
||||
|
||||
payload := []byte(`{
|
||||
"id": "msg_001",
|
||||
"channel_id": "ch_777",
|
||||
"guild_id": "guild_999",
|
||||
"content": "Need help with setup",
|
||||
"author": {
|
||||
"id": "user_456",
|
||||
"username": "gamer_one",
|
||||
"global_name": "Gamer One",
|
||||
"bot": false
|
||||
}
|
||||
}`)
|
||||
|
||||
// 1. Invalid secret
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/third/discord/webhook/"+channel.ChannelID, bytes.NewBuffer(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Discord-Secret-Token", "wrong_secret")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK wrapper, got: %d", rec.Code)
|
||||
}
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["ok"] == true {
|
||||
t.Fatalf("expected error for invalid secret token")
|
||||
}
|
||||
|
||||
// 2. Valid secret
|
||||
req2, _ := http.NewRequest(http.MethodPost, "/api/third/discord/webhook/"+channel.ChannelID, bytes.NewBuffer(payload))
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
req2.Header.Set("X-Discord-Secret-Token", "secret_discord_123")
|
||||
|
||||
rec2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec2, req2)
|
||||
|
||||
if rec2.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK, got %d", rec2.Code)
|
||||
}
|
||||
var resp2 map[string]any
|
||||
_ = json.Unmarshal(rec2.Body.Bytes(), &resp2)
|
||||
if resp2["ok"] != true {
|
||||
t.Fatalf("expected ok: true, got: %+v", resp2)
|
||||
}
|
||||
|
||||
// Verify identity in DB
|
||||
identity := repositories.CustomerIdentityRepository.FindOne(db, sqls.NewCnd().
|
||||
Eq("external_source", enums.ExternalSourceDiscord).
|
||||
Eq("external_id", "user_456"))
|
||||
if identity == nil {
|
||||
t.Fatalf("expected customer identity for user_456")
|
||||
}
|
||||
}
|
||||
115
internal/handlers/third/email_handler.go
Normal file
115
internal/handlers/third/email_handler.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package third
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/httpx"
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// EmailPostWebhook receives incoming inbound email webhook events from Cloudflare, Brevo, SendGrid, Postmark, Mailgun, or SMTP forwarders.
|
||||
func EmailPostWebhook(ctx *gin.Context) {
|
||||
channelID := strings.TrimSpace(ctx.Param("channel_id"))
|
||||
if channelID == "" {
|
||||
channelID = strings.TrimSpace(ctx.Query("channel_id"))
|
||||
}
|
||||
|
||||
secretHeader := ctx.GetHeader("X-Webhook-Secret")
|
||||
if secretHeader == "" {
|
||||
secretHeader = ctx.GetHeader("X-Brevo-Webhook-Secret")
|
||||
}
|
||||
if secretHeader == "" {
|
||||
secretHeader = ctx.GetHeader("X-Postmark-Webhook-Secret")
|
||||
}
|
||||
if secretHeader == "" {
|
||||
secretHeader = ctx.Query("secret")
|
||||
}
|
||||
|
||||
contentType := ctx.GetHeader("Content-Type")
|
||||
|
||||
var formValues url.Values
|
||||
var bodyBytes []byte
|
||||
|
||||
if strings.Contains(strings.ToLower(contentType), "multipart/form-data") {
|
||||
if err := ctx.Request.ParseMultipartForm(32 << 20); err == nil && ctx.Request.MultipartForm != nil {
|
||||
formValues = ctx.Request.MultipartForm.Value
|
||||
}
|
||||
} else if strings.Contains(strings.ToLower(contentType), "application/x-www-form-urlencoded") {
|
||||
if err := ctx.Request.ParseForm(); err == nil {
|
||||
formValues = ctx.Request.PostForm
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Request.Body != nil {
|
||||
bodyBytes, _ = io.ReadAll(ctx.Request.Body)
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
}
|
||||
|
||||
if err := services.EmailInboundService.HandleWebhook(ctx.Request.Context(), channelID, secretHeader, contentType, bodyBytes, formValues); err != nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": true, "message": "email processed"})
|
||||
}
|
||||
|
||||
// EmailPostRequest opens a support request from an external system and continues
|
||||
// the conversation over email.
|
||||
//
|
||||
// It is the API counterpart to EmailPostWebhook: same channel, same identity, but
|
||||
// the caller supplies the fields instead of a parsed email, so an integration
|
||||
// does not have to run a mail forwarder to reach support.
|
||||
//
|
||||
// Presenting X-Webhook-Secret marks the submitted contact details as verified.
|
||||
// Omitting it files the request as self-reported, which agents can see. A wrong
|
||||
// secret is rejected rather than treated as a guest submission.
|
||||
func EmailPostRequest(ctx *gin.Context) {
|
||||
channelID := strings.TrimSpace(ctx.Param("channel_id"))
|
||||
if channelID == "" {
|
||||
channelID = strings.TrimSpace(ctx.Query("channel_id"))
|
||||
}
|
||||
|
||||
req := request.EmailRequestRequest{}
|
||||
if err := params.ReadJSON(ctx, &req); err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := services.EmailRequestService.HandleRequest(
|
||||
ctx.Request.Context(),
|
||||
channelID,
|
||||
webhookSecretFromHeader(ctx),
|
||||
services.EmailRequestInput{
|
||||
Email: req.Email,
|
||||
Name: req.Name,
|
||||
Subject: req.Subject,
|
||||
Message: req.Message,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
httpx.WriteJSON(ctx, err)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(ctx, result)
|
||||
}
|
||||
|
||||
// webhookSecretFromHeader reads the channel secret. The query-string form the
|
||||
// webhook accepts is deliberately not honoured here: this endpoint is meant to be
|
||||
// called by programs, and a secret in a URL leaks into access logs and proxy
|
||||
// history for no benefit.
|
||||
func webhookSecretFromHeader(ctx *gin.Context) string {
|
||||
for _, header := range []string{"X-Webhook-Secret", "X-Breveo-Webhook-Secret", "X-Postmark-Webhook-Secret"} {
|
||||
if value := strings.TrimSpace(ctx.GetHeader(header)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
229
internal/handlers/third/email_handler_test.go
Normal file
229
internal/handlers/third/email_handler_test.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package third
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/repositories"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
func TestEmailPostWebhook_InvalidJSON(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.POST("/webhook", EmailPostWebhook)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewBufferString("invalid-json"))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailPostWebhook_FullFlow(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupThirdHandlerTestDB(t)
|
||||
|
||||
now := time.Now()
|
||||
agent := &models.AIAgent{
|
||||
Name: "Email Support Agent",
|
||||
ServiceMode: enums.IMConversationServiceModeAIFirst,
|
||||
PublishedRevisionID: 1,
|
||||
WelcomeMessage: "Thanks for emailing support.",
|
||||
Status: enums.StatusOk,
|
||||
AuditFields: models.AuditFields{CreatedAt: now, UpdatedAt: now},
|
||||
}
|
||||
_ = db.Create(agent)
|
||||
|
||||
emailConfig, _ := json.Marshal(dto.EmailChannelConfig{
|
||||
EmailAddress: "help@example.com",
|
||||
SenderName: "Support Team",
|
||||
Provider: "brevo",
|
||||
WebhookSecret: "email_secret_token_123",
|
||||
WelcomeMessage: "We have received your email.",
|
||||
})
|
||||
|
||||
operator := &dto.AuthPrincipal{UserID: 1, Username: "admin"}
|
||||
channel, err := services.ChannelService.CreateChannel(request.CreateChannelRequest{
|
||||
Name: "Email Support Channel",
|
||||
ChannelType: enums.ChannelTypeEmail,
|
||||
AIAgentID: agent.ID,
|
||||
AIAgentRolloutPercent: 100,
|
||||
ConfigJSON: string(emailConfig),
|
||||
Status: int(enums.StatusOk),
|
||||
}, operator)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateChannel failed: %v", err)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/third/email/webhook/:channel_id", EmailPostWebhook)
|
||||
router.POST("/api/third/email/webhook", EmailPostWebhook)
|
||||
|
||||
// 1. Test unauthorized when secret doesn't match
|
||||
genericPayload := []byte(`{
|
||||
"from": "alice@customer.com",
|
||||
"from_name": "Alice Customer",
|
||||
"to": "help@example.com",
|
||||
"subject": "Need help with Crove Desk",
|
||||
"text": "Hello, I have an issue with my login credentials.",
|
||||
"message_id": "<msg-001@mail.customer.com>"
|
||||
}`)
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/third/email/webhook/"+channel.ChannelID, bytes.NewBuffer(genericPayload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Webhook-Secret", "wrong_secret")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK wrapper, got: %d", rec.Code)
|
||||
}
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["ok"] != false {
|
||||
t.Fatalf("expected ok: false on wrong secret token, got: %v", resp)
|
||||
}
|
||||
|
||||
// 2. Test successful processing with Generic / Cloudflare payload
|
||||
req2, _ := http.NewRequest(http.MethodPost, "/api/third/email/webhook/"+channel.ChannelID, bytes.NewBuffer(genericPayload))
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
req2.Header.Set("X-Webhook-Secret", "email_secret_token_123")
|
||||
|
||||
rec2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec2, req2)
|
||||
|
||||
var resp2 map[string]any
|
||||
_ = json.Unmarshal(rec2.Body.Bytes(), &resp2)
|
||||
if resp2["ok"] != true {
|
||||
t.Fatalf("expected ok: true, got: %v", resp2)
|
||||
}
|
||||
|
||||
// Verify Customer was created with Email source
|
||||
identity := repositories.CustomerIdentityRepository.FindOne(sqls.DB(), sqls.NewCnd().
|
||||
Eq("external_source", enums.ExternalSourceEmail).
|
||||
Eq("external_id", "alice@customer.com"))
|
||||
if identity == nil {
|
||||
t.Fatalf("expected customer identity for alice@customer.com to be created")
|
||||
}
|
||||
|
||||
customer := repositories.CustomerRepository.Get(sqls.DB(), identity.CustomerID)
|
||||
if customer == nil || customer.PrimaryEmail != "alice@customer.com" {
|
||||
t.Fatalf("expected customer primary_email 'alice@customer.com', got %+v", customer)
|
||||
}
|
||||
|
||||
// Verify Conversation was created
|
||||
conversations := repositories.ConversationRepository.Find(sqls.DB(), sqls.NewCnd().
|
||||
Eq("customer_id", customer.ID).
|
||||
Eq("channel_id", channel.ID))
|
||||
if len(conversations) == 0 {
|
||||
t.Fatalf("expected conversation to be created")
|
||||
}
|
||||
convID := conversations[0].ID
|
||||
|
||||
// Verify Message was saved
|
||||
messages := repositories.MessageRepository.Find(sqls.DB(), sqls.NewCnd().
|
||||
Eq("conversation_id", convID))
|
||||
if len(messages) == 0 {
|
||||
t.Fatalf("expected message to be stored")
|
||||
}
|
||||
|
||||
// 3. Test Threading: Send reply with ticket ID in subject
|
||||
threadedPayload := []byte(strings.ReplaceAll(`{
|
||||
"from": "alice@customer.com",
|
||||
"to": "help@example.com",
|
||||
"subject": "Re: [#CONV_ID] Need help with Crove Desk",
|
||||
"text": "Thanks! Here is additional information.",
|
||||
"message_id": "<msg-002@mail.customer.com>",
|
||||
"in_reply_to": "<msg-001@mail.customer.com>"
|
||||
}`, "CONV_ID", string(rune('0'+convID))))
|
||||
|
||||
reqThread, _ := http.NewRequest(http.MethodPost, "/api/third/email/webhook", bytes.NewBuffer(threadedPayload))
|
||||
reqThread.Header.Set("Content-Type", "application/json")
|
||||
reqThread.Header.Set("X-Webhook-Secret", "email_secret_token_123")
|
||||
|
||||
recThread := httptest.NewRecorder()
|
||||
router.ServeHTTP(recThread, reqThread)
|
||||
|
||||
var respThread map[string]any
|
||||
_ = json.Unmarshal(recThread.Body.Bytes(), &respThread)
|
||||
if respThread["ok"] != true {
|
||||
t.Fatalf("expected threaded ok: true, got: %v", respThread)
|
||||
}
|
||||
|
||||
// Verify message was attached to existing conversation rather than creating a duplicate
|
||||
convCount := len(repositories.ConversationRepository.Find(sqls.DB(), sqls.NewCnd().Eq("customer_id", customer.ID)))
|
||||
if convCount != 1 {
|
||||
t.Fatalf("expected conversation count to remain 1, got %d", convCount)
|
||||
}
|
||||
|
||||
// 4. Test Postmark Inbound Webhook format
|
||||
postmarkPayload := []byte(`{
|
||||
"From": "developer@company.org",
|
||||
"FromName": "Dev Team",
|
||||
"To": "help@example.com",
|
||||
"Subject": "API Inquiry",
|
||||
"TextBody": "How do I call MCP tools?",
|
||||
"MessageID": "postmark-uuid-001"
|
||||
}`)
|
||||
|
||||
reqPM, _ := http.NewRequest(http.MethodPost, "/api/third/email/webhook", bytes.NewBuffer(postmarkPayload))
|
||||
reqPM.Header.Set("Content-Type", "application/json")
|
||||
reqPM.Header.Set("X-Webhook-Secret", "email_secret_token_123")
|
||||
|
||||
recPM := httptest.NewRecorder()
|
||||
router.ServeHTTP(recPM, reqPM)
|
||||
|
||||
var respPM map[string]any
|
||||
_ = json.Unmarshal(recPM.Body.Bytes(), &respPM)
|
||||
if respPM["ok"] != true {
|
||||
t.Fatalf("expected postmark format ok: true, got: %v", respPM)
|
||||
}
|
||||
|
||||
// 5. Test Mailgun Form Data Webhook format
|
||||
mgForm := url.Values{}
|
||||
mgForm.Set("sender", "mailgunner@test.com")
|
||||
mgForm.Set("from", "Mailgun User <mailgunner@test.com>")
|
||||
mgForm.Set("recipient", "help@example.com")
|
||||
mgForm.Set("subject", "Mailgun Inbound Ticket")
|
||||
mgForm.Set("body-plain", "Testing mailgun webhook support.")
|
||||
mgForm.Set("Message-Id", "<mg-998877@mailgun.org>")
|
||||
|
||||
reqMG, _ := http.NewRequest(http.MethodPost, "/api/third/email/webhook", strings.NewReader(mgForm.Encode()))
|
||||
reqMG.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
reqMG.Header.Set("X-Webhook-Secret", "email_secret_token_123")
|
||||
|
||||
recMG := httptest.NewRecorder()
|
||||
router.ServeHTTP(recMG, reqMG)
|
||||
|
||||
var respMG map[string]any
|
||||
_ = json.Unmarshal(recMG.Body.Bytes(), &respMG)
|
||||
if respMG["ok"] != true {
|
||||
t.Fatalf("expected mailgun format ok: true, got: %v", respMG)
|
||||
}
|
||||
|
||||
mgIdentity := repositories.CustomerIdentityRepository.FindOne(sqls.DB(), sqls.NewCnd().
|
||||
Eq("external_source", enums.ExternalSourceEmail).
|
||||
Eq("external_id", "mailgunner@test.com"))
|
||||
if mgIdentity == nil {
|
||||
t.Fatalf("expected customer identity for mailgunner@test.com")
|
||||
}
|
||||
}
|
||||
43
internal/handlers/third/slack_handler.go
Normal file
43
internal/handlers/third/slack_handler.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package third
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SlackPostWebhook receives incoming Events API payloads from Slack.
|
||||
func SlackPostWebhook(ctx *gin.Context) {
|
||||
channelID := strings.TrimSpace(ctx.Param("channel_id"))
|
||||
if channelID == "" {
|
||||
channelID = strings.TrimSpace(ctx.Query("channel_id"))
|
||||
}
|
||||
|
||||
timestampHeader := ctx.GetHeader("X-Slack-Request-Timestamp")
|
||||
signatureHeader := ctx.GetHeader("X-Slack-Signature")
|
||||
|
||||
bodyBytes, err := io.ReadAll(ctx.Request.Body)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "failed to read body"})
|
||||
return
|
||||
}
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
challenge, err := services.SlackInboundService.HandleWebhook(ctx.Request.Context(), channelID, timestampHeader, signatureHeader, bodyBytes)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if challenge != nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"challenge": *challenge})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
153
internal/handlers/third/slack_handler_test.go
Normal file
153
internal/handlers/third/slack_handler_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package third
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/repositories"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
const slackHandlerTestSigningSecret = "test_signing_secret"
|
||||
|
||||
// signSlackHandlerPayload builds the X-Slack-Request-Timestamp and
|
||||
// X-Slack-Signature headers Slack sends for this body right now.
|
||||
func signSlackHandlerPayload(payload []byte) (string, string) {
|
||||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
mac := hmac.New(sha256.New, []byte(slackHandlerTestSigningSecret))
|
||||
mac.Write([]byte("v0:" + timestamp + ":" + string(payload)))
|
||||
return timestamp, "v0=" + hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func TestSlackWebhook_Handler(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupThirdHandlerTestDB(t)
|
||||
|
||||
now := time.Now()
|
||||
agent := &models.AIAgent{
|
||||
Name: "Slack Agent",
|
||||
ServiceMode: enums.IMConversationServiceModeAIFirst,
|
||||
PublishedRevisionID: 1,
|
||||
WelcomeMessage: "Hello Slack User!",
|
||||
Status: enums.StatusOk,
|
||||
AuditFields: models.AuditFields{CreatedAt: now, UpdatedAt: now},
|
||||
}
|
||||
if err := db.Create(agent).Error; err != nil {
|
||||
t.Fatalf("create ai agent: %v", err)
|
||||
}
|
||||
|
||||
slackConfig, err := json.Marshal(dto.SlackChannelConfig{
|
||||
BotToken: "xoxb-test-token",
|
||||
SigningSecret: slackHandlerTestSigningSecret,
|
||||
TeamID: "T_SLACK_100",
|
||||
DefaultChannel: "C_GENERAL",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal slack config: %v", err)
|
||||
}
|
||||
|
||||
operator := &dto.AuthPrincipal{UserID: 1, Username: "admin"}
|
||||
channel, err := services.ChannelService.CreateChannel(request.CreateChannelRequest{
|
||||
Name: "Slack Channel",
|
||||
ChannelType: enums.ChannelTypeSlack,
|
||||
AIAgentID: agent.ID,
|
||||
AIAgentRolloutPercent: 100,
|
||||
ConfigJSON: string(slackConfig),
|
||||
Status: int(enums.StatusOk),
|
||||
}, operator)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateChannel failed: %v", err)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/third/slack/webhook/:channel_id", SlackPostWebhook)
|
||||
router.POST("/api/third/slack/webhook", SlackPostWebhook)
|
||||
|
||||
post := func(path string, payload []byte, timestamp, signature string) *httptest.ResponseRecorder {
|
||||
req, _ := http.NewRequest(http.MethodPost, path, bytes.NewBuffer(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if timestamp != "" {
|
||||
req.Header.Set("X-Slack-Request-Timestamp", timestamp)
|
||||
}
|
||||
if signature != "" {
|
||||
req.Header.Set("X-Slack-Signature", signature)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
webhookPath := "/api/third/slack/webhook/" + channel.ChannelID
|
||||
|
||||
// 1. The url_verification handshake is answered without a signature, because
|
||||
// Slack sends it once while the endpoint is being configured.
|
||||
challengePayload := []byte(`{
|
||||
"token": "token123",
|
||||
"challenge": "slack_challenge_string_999",
|
||||
"type": "url_verification"
|
||||
}`)
|
||||
recChallenge := post(webhookPath, challengePayload, "", "")
|
||||
if recChallenge.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK for challenge, got: %d", recChallenge.Code)
|
||||
}
|
||||
var challengeResp map[string]any
|
||||
if err := json.Unmarshal(recChallenge.Body.Bytes(), &challengeResp); err != nil {
|
||||
t.Fatalf("unmarshal challenge response: %v", err)
|
||||
}
|
||||
if challengeResp["challenge"] != "slack_challenge_string_999" {
|
||||
t.Fatalf("expected challenge in body, got: %+v", challengeResp)
|
||||
}
|
||||
|
||||
// 2. An event the channel's signing secret did not produce is rejected, and
|
||||
// must not create a customer identity.
|
||||
eventPayload := []byte(`{
|
||||
"token": "token123",
|
||||
"team_id": "T_SLACK_100",
|
||||
"type": "event_callback",
|
||||
"event": {
|
||||
"type": "message",
|
||||
"user": "U_USER_777",
|
||||
"text": "Hello support team on Slack!",
|
||||
"ts": "1725260000.000100",
|
||||
"channel": "C_GENERAL"
|
||||
}
|
||||
}`)
|
||||
recUnsigned := post(webhookPath, eventPayload, "", "")
|
||||
if recUnsigned.Code != http.StatusOK {
|
||||
t.Fatalf("expected the handler to answer 200 with an error body, got: %d", recUnsigned.Code)
|
||||
}
|
||||
if repositories.CustomerIdentityRepository.FindOne(db, sqls.NewCnd().
|
||||
Eq("external_source", enums.ExternalSourceSlack).
|
||||
Eq("external_id", "U_USER_777")) != nil {
|
||||
t.Fatalf("an unsigned event created a customer identity")
|
||||
}
|
||||
|
||||
// 3. A correctly signed event is accepted and resolves the sender.
|
||||
timestamp, signature := signSlackHandlerPayload(eventPayload)
|
||||
recEvent := post(webhookPath, eventPayload, timestamp, signature)
|
||||
if recEvent.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK for event, got: %d", recEvent.Code)
|
||||
}
|
||||
|
||||
identity := repositories.CustomerIdentityRepository.FindOne(db, sqls.NewCnd().
|
||||
Eq("external_source", enums.ExternalSourceSlack).
|
||||
Eq("external_id", "U_USER_777"))
|
||||
if identity == nil {
|
||||
t.Fatalf("expected customer identity for U_USER_777")
|
||||
}
|
||||
}
|
||||
36
internal/handlers/third/telegram_handler.go
Normal file
36
internal/handlers/third/telegram_handler.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package third
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TelegramPostWebhook receives incoming Webhook events from Telegram Bot API.
|
||||
func TelegramPostWebhook(ctx *gin.Context) {
|
||||
channelID := strings.TrimSpace(ctx.Param("channel_id"))
|
||||
if channelID == "" {
|
||||
channelID = strings.TrimSpace(ctx.Query("channel_id"))
|
||||
}
|
||||
|
||||
secretHeader := ctx.GetHeader("X-Telegram-Bot-Api-Secret-Token")
|
||||
|
||||
bodyBytes, err := io.ReadAll(ctx.Request.Body)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "failed to read body"})
|
||||
return
|
||||
}
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
if err := services.TelegramInboundService.HandleWebhook(ctx.Request.Context(), channelID, secretHeader, bodyBytes); err != nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
150
internal/handlers/third/telegram_handler_test.go
Normal file
150
internal/handlers/third/telegram_handler_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package third
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/repositories"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
func setupThirdHandlerTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: "t_",
|
||||
SingularTable: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite db: %v", err)
|
||||
}
|
||||
_ = db.AutoMigrate(
|
||||
&models.Channel{},
|
||||
&models.ChannelMessageOutbox{},
|
||||
&models.Customer{},
|
||||
&models.CustomerIdentity{},
|
||||
&models.CustomerContact{},
|
||||
&models.Conversation{},
|
||||
&models.ConversationParticipant{},
|
||||
&models.ConversationReadState{},
|
||||
&models.ConversationEventLog{},
|
||||
&models.ConversationInterrupt{},
|
||||
&models.Message{},
|
||||
&models.AIAgent{},
|
||||
&models.User{},
|
||||
&models.Role{},
|
||||
&models.UserRole{},
|
||||
&models.Permission{},
|
||||
&models.RolePermission{},
|
||||
&models.UserPermission{},
|
||||
)
|
||||
sqls.SetDB(db)
|
||||
return db
|
||||
}
|
||||
|
||||
func TestTelegramPostWebhook_Handler(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupThirdHandlerTestDB(t)
|
||||
|
||||
now := time.Now()
|
||||
agent := &models.AIAgent{
|
||||
Name: "Telegram Bot Agent",
|
||||
ServiceMode: enums.IMConversationServiceModeAIFirst,
|
||||
PublishedRevisionID: 1,
|
||||
WelcomeMessage: "Hello!",
|
||||
Status: enums.StatusOk,
|
||||
AuditFields: models.AuditFields{CreatedAt: now, UpdatedAt: now},
|
||||
}
|
||||
_ = db.Create(agent)
|
||||
|
||||
tgConfig, _ := json.Marshal(dto.TelegramChannelConfig{
|
||||
BotToken: "123456:ABC-DEF",
|
||||
BotUsername: "test_bot",
|
||||
WebhookSecret: "my_secret_token_123",
|
||||
WelcomeMessage: "Welcome!",
|
||||
})
|
||||
|
||||
operator := &dto.AuthPrincipal{UserID: 1, Username: "admin"}
|
||||
channel, err := services.ChannelService.CreateChannel(request.CreateChannelRequest{
|
||||
Name: "Telegram Channel",
|
||||
ChannelType: enums.ChannelTypeTelegram,
|
||||
AIAgentID: agent.ID,
|
||||
AIAgentRolloutPercent: 100,
|
||||
ConfigJSON: string(tgConfig),
|
||||
Status: int(enums.StatusOk),
|
||||
}, operator)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateChannel failed: %v", err)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/third/telegram/webhook/:channel_id", TelegramPostWebhook)
|
||||
router.POST("/api/third/telegram/webhook", TelegramPostWebhook)
|
||||
|
||||
// 1. Test unauthorized when secret doesn't match
|
||||
updatePayload := []byte(`{
|
||||
"update_id": 112233,
|
||||
"message": {
|
||||
"message_id": 999,
|
||||
"from": {"id": 777888, "first_name": "Alice"},
|
||||
"chat": {"id": 777888, "type": "private"},
|
||||
"text": "Hello from Telegram!"
|
||||
}
|
||||
}`)
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/third/telegram/webhook/"+channel.ChannelID, bytes.NewBuffer(updatePayload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Telegram-Bot-Api-Secret-Token", "wrong_token")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK wrapper, got: %d", rec.Code)
|
||||
}
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["ok"] == true {
|
||||
t.Fatalf("expected error for invalid secret token")
|
||||
}
|
||||
|
||||
// 2. Test success with valid secret
|
||||
req2, _ := http.NewRequest(http.MethodPost, "/api/third/telegram/webhook/"+channel.ChannelID, bytes.NewBuffer(updatePayload))
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
req2.Header.Set("X-Telegram-Bot-Api-Secret-Token", "my_secret_token_123")
|
||||
|
||||
rec2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec2, req2)
|
||||
|
||||
if rec2.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK, got %d", rec2.Code)
|
||||
}
|
||||
var resp2 map[string]any
|
||||
_ = json.Unmarshal(rec2.Body.Bytes(), &resp2)
|
||||
if resp2["ok"] != true {
|
||||
t.Fatalf("expected ok: true, got: %+v", resp2)
|
||||
}
|
||||
|
||||
// Verify message in database
|
||||
identity := repositories.CustomerIdentityRepository.FindOne(db, sqls.NewCnd().
|
||||
Eq("external_source", enums.ExternalSourceTelegram).
|
||||
Eq("external_id", "777888"))
|
||||
if identity == nil {
|
||||
t.Fatalf("expected customer identity for 777888")
|
||||
}
|
||||
}
|
||||
39
internal/handlers/third/zalo_handler.go
Normal file
39
internal/handlers/third/zalo_handler.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package third
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ZaloPostWebhook receives incoming Webhook events from Zalo Official Account.
|
||||
func ZaloPostWebhook(ctx *gin.Context) {
|
||||
channelID := strings.TrimSpace(ctx.Param("channel_id"))
|
||||
if channelID == "" {
|
||||
channelID = strings.TrimSpace(ctx.Query("channel_id"))
|
||||
}
|
||||
|
||||
sigHeader := ctx.GetHeader("X-Zalo-Signature")
|
||||
if sigHeader == "" {
|
||||
sigHeader = ctx.GetHeader("X-Hub-Signature")
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(ctx.Request.Body)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": 1, "message": "failed to read body"})
|
||||
return
|
||||
}
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
if err := services.ZaloOAInboundService.HandleWebhook(ctx.Request.Context(), channelID, sigHeader, bodyBytes); err != nil {
|
||||
ctx.JSON(http.StatusOK, gin.H{"error": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{"error": 0, "message": "Success"})
|
||||
}
|
||||
105
internal/handlers/third/zalo_handler_test.go
Normal file
105
internal/handlers/third/zalo_handler_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package third
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/repositories"
|
||||
"agent-desk/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
)
|
||||
|
||||
func TestZaloPostWebhook_Handler(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupThirdHandlerTestDB(t)
|
||||
|
||||
now := time.Now()
|
||||
agent := &models.AIAgent{
|
||||
Name: "Zalo OA Bot Agent",
|
||||
ServiceMode: enums.IMConversationServiceModeAIFirst,
|
||||
PublishedRevisionID: 1,
|
||||
WelcomeMessage: "Xin chào từ Zalo OA!",
|
||||
Status: enums.StatusOk,
|
||||
AuditFields: models.AuditFields{CreatedAt: now, UpdatedAt: now},
|
||||
}
|
||||
_ = db.Create(agent)
|
||||
|
||||
zaloConfig, _ := json.Marshal(dto.ZaloOAChannelConfig{
|
||||
AppID: "1234567890",
|
||||
OAID: "9876543210",
|
||||
AccessToken: "zalo_oa_live_token_abc",
|
||||
WebhookSecret: "my_zalo_secret",
|
||||
})
|
||||
|
||||
operator := &dto.AuthPrincipal{UserID: 1, Username: "admin"}
|
||||
channel, err := services.ChannelService.CreateChannel(request.CreateChannelRequest{
|
||||
Name: "Zalo OA Channel",
|
||||
ChannelType: enums.ChannelTypeZaloOA,
|
||||
AIAgentID: agent.ID,
|
||||
AIAgentRolloutPercent: 100,
|
||||
ConfigJSON: string(zaloConfig),
|
||||
Status: int(enums.StatusOk),
|
||||
}, operator)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateChannel failed: %v", err)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.POST("/api/third/zalo/webhook/:channel_id", ZaloPostWebhook)
|
||||
router.POST("/api/third/zalo/webhook", ZaloPostWebhook)
|
||||
|
||||
// Inbound message payload
|
||||
updatePayload := []byte(`{
|
||||
"event_name": "user_send_text",
|
||||
"app_id": "1234567890",
|
||||
"sender": {"id": "zalo_uid_999"},
|
||||
"recipient": {"id": "9876543210"},
|
||||
"message": {
|
||||
"msg_id": "zalo_msg_777",
|
||||
"text": "Chào bạn, tôi cần tư vấn mua license Crove Desk"
|
||||
},
|
||||
"info": {
|
||||
"display_name": "Nguyen Van C"
|
||||
},
|
||||
"timestamp": "1756201000"
|
||||
}`)
|
||||
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/third/zalo/webhook/"+channel.ChannelID, bytes.NewBuffer(updatePayload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Zalo-Signature", "my_zalo_secret")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK, got %d", rec.Code)
|
||||
}
|
||||
var resp map[string]any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
|
||||
if resp["error"] != float64(0) {
|
||||
t.Fatalf("expected error: 0, got: %+v", resp)
|
||||
}
|
||||
|
||||
// Verify customer identity created
|
||||
identity := repositories.CustomerIdentityRepository.FindOne(db, sqls.NewCnd().
|
||||
Eq("external_source", enums.ExternalSourceZaloOA).
|
||||
Eq("external_id", "zalo_uid_999"))
|
||||
if identity == nil {
|
||||
t.Fatalf("expected customer identity for zalo_uid_999")
|
||||
}
|
||||
|
||||
customer := repositories.CustomerRepository.Get(db, identity.CustomerID)
|
||||
if customer == nil || customer.Name != "Nguyen Van C" {
|
||||
t.Fatalf("unexpected customer: %+v", customer)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user