chore: import clean Agent Desk snapshot (2/7)

This commit is contained in:
2026-10-16 11:57:21 +00:00
parent 79176541c7
commit 89c5231c4c
461 changed files with 60092 additions and 0 deletions

View 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
}

View 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")
}
}

View 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)
}
}

View 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))
}

View 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")
}
})
}

View 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)
}

View 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"})
}

View 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))
}

View 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())
}

View 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
}

View 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 ""
}

View 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)
}

View 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)
}

View 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
}

View 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
}

View 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
}

View 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)
}

View 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)
}

View 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))
}

View 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)
}

View 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)
}

View 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
}

View 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)
}

View 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)
}

View 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})
}

View 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)
}
})
}

View 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)
}

View 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)
}

View 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)))
}

View 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)
}

View 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)
}

View 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)
}

View 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
}

View 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"))
}

View File

@@ -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,
})
}

View 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))
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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
}

View 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)
}

View 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))
}

View 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)
}

View 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})
}

View 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")
}
}

View 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 ""
}

View 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")
}
}

View 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})
}

View 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")
}
}

View 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})
}

View 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")
}
}

View 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"})
}

View 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)
}
}