refactor: split backend into standalone server project
Remove embedded frontend and workflow editor assets, add standalone API deployment configuration, and retain the current backend service updates.
This commit is contained in:
@@ -315,9 +315,8 @@ func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRe
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !slices.Contains(enums.IMConversationServiceModeValues, req.ServiceMode) {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0230")
|
||||
}
|
||||
// AI Agent 只描述 AI 的能力与转人工策略;渠道决定使用人工还是 AI 接待。
|
||||
req.ServiceMode = enums.IMConversationServiceModeAIFirst
|
||||
teamIDs, err := s.normalizeTeamIDs(req.TeamIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -44,6 +44,19 @@ func TestUpdateAIAgentKeepsPublishedRevisionActive(t *testing.T) {
|
||||
if err := db.Create(config).Error; err != nil {
|
||||
t.Fatalf("create ai config: %v", err)
|
||||
}
|
||||
createdModel, err := AIAgentService.buildAIAgentModel(0, request.CreateAIAgentRequest{
|
||||
Name: "fixed ai reception agent",
|
||||
AIConfigID: config.ID,
|
||||
ServiceMode: enums.IMConversationServiceModeHumanOnly,
|
||||
HandoffMode: enums.AIAgentHandoffModeWaitPool,
|
||||
FallbackMode: enums.AIAgentFallbackModeNoAnswer,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("build ai agent model: %v", err)
|
||||
}
|
||||
if createdModel.ServiceMode != enums.IMConversationServiceModeAIFirst {
|
||||
t.Fatalf("service mode = %d, want AI first", createdModel.ServiceMode)
|
||||
}
|
||||
agent := &models.AIAgent{
|
||||
Name: "published agent", Status: enums.StatusOk, AIConfigID: config.ID,
|
||||
ServiceMode: enums.IMConversationServiceModeAIFirst, HandoffMode: enums.AIAgentHandoffModeWaitPool,
|
||||
|
||||
@@ -340,7 +340,7 @@ func (s *authService) loadUserPermissionCodes(tx *gorm.DB, userID int64) ([]stri
|
||||
Joins("JOIN t_user_role AS ur ON ur.role_id = rp.role_id").
|
||||
Where("ur.user_id = ?", userID).
|
||||
Where("p.status = ?", enums.StatusOk)
|
||||
if err := db.Order("p.sort_no ASC, p.id ASC").Scan(&permissionRows).Error; err != nil {
|
||||
if err := db.Scan(&permissionRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -368,6 +368,54 @@ func TestAuthServiceLogoutRevokesCurrentTokenOnly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadUserPermissionCodesReturnsSortedDistinctCodes(t *testing.T) {
|
||||
db := setupAuthServiceTestDB(t)
|
||||
user := createAuthTestUser(t, db, "admin", "secret")
|
||||
now := time.Now()
|
||||
auditFields := models.AuditFields{CreatedAt: now, UpdatedAt: now}
|
||||
|
||||
permissions := []models.Permission{
|
||||
{Name: "Zulu", Code: "z.read", SortNo: 1, Status: enums.StatusOk, AuditFields: auditFields},
|
||||
{Name: "Alpha", Code: "a.read", SortNo: 2, Status: enums.StatusOk, AuditFields: auditFields},
|
||||
}
|
||||
if err := db.Create(&permissions).Error; err != nil {
|
||||
t.Fatalf("seed permissions: %v", err)
|
||||
}
|
||||
|
||||
roles := []models.Role{
|
||||
{Name: "Role One", Code: "role_one", Status: enums.StatusOk, AuditFields: auditFields},
|
||||
{Name: "Role Two", Code: "role_two", Status: enums.StatusOk, AuditFields: auditFields},
|
||||
}
|
||||
if err := db.Create(&roles).Error; err != nil {
|
||||
t.Fatalf("seed roles: %v", err)
|
||||
}
|
||||
|
||||
userRoles := []models.UserRole{
|
||||
{UserID: user.ID, RoleID: roles[0].ID, AuditFields: auditFields},
|
||||
{UserID: user.ID, RoleID: roles[1].ID, AuditFields: auditFields},
|
||||
}
|
||||
if err := db.Create(&userRoles).Error; err != nil {
|
||||
t.Fatalf("seed user roles: %v", err)
|
||||
}
|
||||
|
||||
rolePermissions := []models.RolePermission{
|
||||
{RoleID: roles[0].ID, PermissionID: permissions[0].ID, AuditFields: auditFields},
|
||||
{RoleID: roles[0].ID, PermissionID: permissions[1].ID, AuditFields: auditFields},
|
||||
{RoleID: roles[1].ID, PermissionID: permissions[0].ID, AuditFields: auditFields},
|
||||
}
|
||||
if err := db.Create(&rolePermissions).Error; err != nil {
|
||||
t.Fatalf("seed role permissions: %v", err)
|
||||
}
|
||||
|
||||
codes, err := newAuthService().loadUserPermissionCodes(db, user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("load user permission codes: %v", err)
|
||||
}
|
||||
if got, want := strings.Join(codes, ","), "a.read,z.read"; got != want {
|
||||
t.Fatalf("permission codes = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func setupAuthServiceTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{
|
||||
|
||||
@@ -423,21 +423,20 @@ func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRe
|
||||
if name == "" {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0247")
|
||||
}
|
||||
if req.AIAgentID <= 0 {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0321")
|
||||
}
|
||||
if req.AIAgentRolloutPercent == 0 {
|
||||
req.AIAgentRolloutPercent = 100
|
||||
}
|
||||
if req.AIAgentRolloutPercent < 1 || req.AIAgentRolloutPercent > 100 {
|
||||
return nil, errorsx.InvalidParam("channel ai agent rollout percent must be between 1 and 100")
|
||||
}
|
||||
aiAgent := AIAgentService.Get(req.AIAgentID)
|
||||
if aiAgent == nil || aiAgent.Status != enums.StatusOk {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0004")
|
||||
}
|
||||
if aiAgent.PublishedRevisionID <= 0 {
|
||||
return nil, errorsx.InvalidParam("ai agent must be published before binding channel")
|
||||
if req.AIAgentID > 0 {
|
||||
aiAgent := AIAgentService.Get(req.AIAgentID)
|
||||
if aiAgent == nil || aiAgent.Status != enums.StatusOk {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0004")
|
||||
}
|
||||
if aiAgent.PublishedRevisionID <= 0 {
|
||||
return nil, errorsx.InvalidParam("ai agent must be published before binding channel")
|
||||
}
|
||||
}
|
||||
status := enums.Status(req.Status)
|
||||
if req.Status == 0 {
|
||||
|
||||
@@ -48,6 +48,23 @@ func TestChannelServiceAllowsAgentWithPublishedRevision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelServiceAllowsHumanReceptionWithoutAIAgent(t *testing.T) {
|
||||
setupChannelServiceTestDB(t)
|
||||
|
||||
item, err := ChannelService.CreateChannel(request.CreateChannelRequest{
|
||||
ChannelType: enums.ChannelTypeWeb,
|
||||
AIAgentID: 0,
|
||||
Name: "人工客服渠道",
|
||||
Status: int(enums.StatusOk),
|
||||
}, channelServiceTestOperator())
|
||||
if err != nil {
|
||||
t.Fatalf("create human channel: %v", err)
|
||||
}
|
||||
if item == nil || item.AIAgentID != 0 {
|
||||
t.Fatalf("unexpected human channel: %#v", item)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelServiceStoresAIAgentRolloutPercent(t *testing.T) {
|
||||
db := setupChannelServiceTestDB(t)
|
||||
agent := createChannelServiceTestAgent(t, db, 1001)
|
||||
|
||||
@@ -116,6 +116,16 @@ func (s *conversationHumanDispatchService) ApplyHumanOnlyCreate(conversationID i
|
||||
return s.dispatchAfterHandoff(conversationID, aiAgent.ID, activeTeamIDs, "仅人工模式新会话", false)
|
||||
}
|
||||
|
||||
func (s *conversationHumanDispatchService) ApplyHumanChannelCreate(conversationID int64) (*HandoffDecisionResult, error) {
|
||||
if err := s.moveToGlobalPool(conversationID, "人工客服渠道"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.sendAIText(conversationID, 0, HandoffWaitingMessage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &HandoffDecisionResult{Decision: HandoffDecisionGlobalPool, Message: HandoffWaitingMessage}, nil
|
||||
}
|
||||
|
||||
func (s *conversationHumanDispatchService) DispatchPendingConversation(conversationID int64, aiAgent models.AIAgent) (*HandoffDecisionResult, error) {
|
||||
conversation := ConversationService.Get(conversationID)
|
||||
if conversation == nil {
|
||||
|
||||
@@ -101,9 +101,14 @@ func (s *conversationService) getLatestNotFinishedByCustomerID(db *gorm.DB, cust
|
||||
}
|
||||
|
||||
func (s *conversationService) Create(externalUser openidentity.ExternalUser, channelID, aiAgentID int64) (*models.Conversation, error) {
|
||||
aiAgent := AIAgentService.Get(aiAgentID)
|
||||
if aiAgent == nil || aiAgent.Status != enums.StatusOk {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0002")
|
||||
serviceMode := enums.IMConversationServiceModeHumanOnly
|
||||
var aiAgent *models.AIAgent
|
||||
if aiAgentID > 0 {
|
||||
aiAgent = AIAgentService.Get(aiAgentID)
|
||||
if aiAgent == nil || aiAgent.Status != enums.StatusOk {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0002")
|
||||
}
|
||||
serviceMode = aiAgent.ServiceMode
|
||||
}
|
||||
|
||||
var conversation *models.Conversation
|
||||
@@ -135,8 +140,8 @@ func (s *conversationService) Create(externalUser openidentity.ExternalUser, cha
|
||||
ChannelID: channelID,
|
||||
CustomerID: customerID,
|
||||
CustomerName: customerName,
|
||||
Status: s.resolveInitialStatus(aiAgent.ServiceMode),
|
||||
ServiceMode: aiAgent.ServiceMode,
|
||||
Status: s.resolveInitialStatus(serviceMode),
|
||||
ServiceMode: serviceMode,
|
||||
Priority: 0,
|
||||
CurrentAssigneeID: 0,
|
||||
CurrentTeamID: 0,
|
||||
@@ -153,8 +158,11 @@ func (s *conversationService) Create(externalUser openidentity.ExternalUser, cha
|
||||
if err := ConversationEventLogService.CreateEvent(ctx, conversation.ID, enums.IMEventTypeCreate, enums.IMSenderTypeCustomer, 0, "用户创建会话", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
welcomeMessage, err = MessageService.createAIWelcomeMessage(ctx, conversation, aiAgent, now)
|
||||
return err
|
||||
if aiAgent != nil {
|
||||
welcomeMessage, err = MessageService.createAIWelcomeMessage(ctx, conversation, aiAgent, now)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -174,8 +182,14 @@ func (s *conversationService) Create(externalUser openidentity.ExternalUser, cha
|
||||
}
|
||||
}
|
||||
|
||||
if aiAgent.ServiceMode == enums.IMConversationServiceModeHumanOnly {
|
||||
if _, err := ConversationHumanDispatchService.ApplyHumanOnlyCreate(conversation.ID, *aiAgent); err != nil {
|
||||
if serviceMode == enums.IMConversationServiceModeHumanOnly {
|
||||
var err error
|
||||
if aiAgent == nil {
|
||||
_, err = ConversationHumanDispatchService.ApplyHumanChannelCreate(conversation.ID)
|
||||
} else {
|
||||
_, err = ConversationHumanDispatchService.ApplyHumanOnlyCreate(conversation.ID, *aiAgent)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,35 @@ func TestConversationCreateCreatesAIWelcomeMessage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConversationCreateAllowsHumanChannelWithoutAIAgent(t *testing.T) {
|
||||
db := setupMessageWelcomeTestDB(t)
|
||||
|
||||
conversation, err := ConversationService.Create(welcomeTestExternalUser("human-channel-1"), 11, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("create human channel conversation: %v", err)
|
||||
}
|
||||
if conversation == nil {
|
||||
t.Fatal("expected conversation")
|
||||
}
|
||||
if conversation.AIAgentID != 0 {
|
||||
t.Fatalf("ai agent id = %d, want 0", conversation.AIAgentID)
|
||||
}
|
||||
if conversation.ServiceMode != enums.IMConversationServiceModeHumanOnly {
|
||||
t.Fatalf("service mode = %d, want human only", conversation.ServiceMode)
|
||||
}
|
||||
if conversation.Status != enums.IMConversationStatusPending {
|
||||
t.Fatalf("status = %d, want pending", conversation.Status)
|
||||
}
|
||||
|
||||
var messages []models.Message
|
||||
if err := db.Where("conversation_id = ?", conversation.ID).Find(&messages).Error; err != nil {
|
||||
t.Fatalf("find messages: %v", err)
|
||||
}
|
||||
if len(messages) != 1 || messages[0].Content != HandoffWaitingMessage {
|
||||
t.Fatalf("unexpected human channel messages: %#v", messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendCustomerMessageStoresRequestIDOnMessageAndEvent(t *testing.T) {
|
||||
db := setupMessageWelcomeTestDB(t)
|
||||
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
||||
|
||||
@@ -548,13 +548,6 @@ func (s *wxWorkKFInboundService) getChannelByOpenKfID(openKfID string) (*models.
|
||||
if channel == nil {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0231")
|
||||
}
|
||||
if channel.AIAgentID <= 0 {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0098")
|
||||
}
|
||||
agent := AIAgentService.Get(channel.AIAgentID)
|
||||
if agent == nil || agent.Status != enums.StatusOk {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0099")
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user