refactor(ticket): simplify ticket number generation and remove SQLite lock
This commit is contained in:
@@ -1,130 +0,0 @@
|
|||||||
package executor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"cs-agent/internal/ai/rag"
|
|
||||||
"cs-agent/internal/ai/runtime/internal/impl/retrievers"
|
|
||||||
"cs-agent/internal/models"
|
|
||||||
"cs-agent/internal/pkg/enums"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBuildKnowledgeGuardDecisionFallsBackWhenKnowledgeMisses(t *testing.T) {
|
|
||||||
agent := newKnowledgeGuardAgentFixture()
|
|
||||||
agent.FallbackMode = enums.AIAgentFallbackModeSuggestRetry
|
|
||||||
decision := buildKnowledgeGuardDecision(agent, &retrievers.KnowledgeRetrieveResult{
|
|
||||||
KnowledgeBaseIDs: []int64{1},
|
|
||||||
})
|
|
||||||
|
|
||||||
if decision.FallbackReply != "当前知识库里没有找到足够明确的信息,你可以换个更具体的问法再试一次。" {
|
|
||||||
t.Fatalf("unexpected fallback reply: %q", decision.FallbackReply)
|
|
||||||
}
|
|
||||||
if len(decision.Instructions) != 0 {
|
|
||||||
t.Fatalf("expected no instructions on miss, got %d", len(decision.Instructions))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildKnowledgeGuardDecisionUsesAgentFallbackMessage(t *testing.T) {
|
|
||||||
agent := newKnowledgeGuardAgentFixture()
|
|
||||||
agent.FallbackMessage = "请联系人工客服"
|
|
||||||
decision := buildKnowledgeGuardDecision(agent, &retrievers.KnowledgeRetrieveResult{
|
|
||||||
KnowledgeBaseIDs: []int64{1},
|
|
||||||
})
|
|
||||||
|
|
||||||
if decision.FallbackReply != "请联系人工客服" {
|
|
||||||
t.Fatalf("expected agent fallback message, got %q", decision.FallbackReply)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildKnowledgeGuardDecisionInjectsStrictInstructionOnHit(t *testing.T) {
|
|
||||||
agent := newKnowledgeGuardAgentFixture()
|
|
||||||
decision := buildKnowledgeGuardDecision(agent, &retrievers.KnowledgeRetrieveResult{
|
|
||||||
KnowledgeBaseIDs: []int64{1},
|
|
||||||
Hits: []rag.RetrieveResult{
|
|
||||||
{KnowledgeBaseID: 1, Score: 0.88},
|
|
||||||
},
|
|
||||||
ContextText: "知识库上下文",
|
|
||||||
AnswerMode: enums.KnowledgeAnswerModeStrict,
|
|
||||||
})
|
|
||||||
|
|
||||||
if decision.FallbackReply != "" {
|
|
||||||
t.Fatalf("expected no fallback reply on hit, got %q", decision.FallbackReply)
|
|
||||||
}
|
|
||||||
if len(decision.Instructions) != 1 {
|
|
||||||
t.Fatalf("expected one instruction, got %d", len(decision.Instructions))
|
|
||||||
}
|
|
||||||
content := decision.Instructions[0].Content
|
|
||||||
if !strings.Contains(content, "只能依据后续提供的知识片段回答") {
|
|
||||||
t.Fatalf("unexpected strict instruction: %q", content)
|
|
||||||
}
|
|
||||||
if !strings.Contains(content, "当前知识库暂无明确信息。") {
|
|
||||||
t.Fatalf("expected fallback text in instruction, got %q", content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildKnowledgeGuardDecisionFallsBackWhenHitHasNoContext(t *testing.T) {
|
|
||||||
agent := newKnowledgeGuardAgentFixture()
|
|
||||||
agent.FallbackMessage = "我暂时没有找到足够准确的信息。"
|
|
||||||
decision := buildKnowledgeGuardDecision(agent, &retrievers.KnowledgeRetrieveResult{
|
|
||||||
KnowledgeBaseIDs: []int64{1},
|
|
||||||
Hits: []rag.RetrieveResult{
|
|
||||||
{KnowledgeBaseID: 1, Score: 0.88},
|
|
||||||
},
|
|
||||||
AnswerMode: enums.KnowledgeAnswerModeStrict,
|
|
||||||
})
|
|
||||||
|
|
||||||
if decision.FallbackReply != "我暂时没有找到足够准确的信息。" {
|
|
||||||
t.Fatalf("expected fallback on empty context, got %q", decision.FallbackReply)
|
|
||||||
}
|
|
||||||
if len(decision.Instructions) != 0 {
|
|
||||||
t.Fatalf("expected no instructions on empty context, got %d", len(decision.Instructions))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildKnowledgeUnavailableDecisionFallsBackWhenAgentHasKnowledge(t *testing.T) {
|
|
||||||
agent := newKnowledgeGuardAgentFixture()
|
|
||||||
agent.FallbackMessage = "知识库暂时不可用。"
|
|
||||||
decision := buildKnowledgeUnavailableDecision(agent, []int64{1})
|
|
||||||
|
|
||||||
if decision.FallbackReply != "知识库暂时不可用。" {
|
|
||||||
t.Fatalf("expected fallback when knowledge unavailable, got %q", decision.FallbackReply)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildKnowledgeUnavailableDecisionSkipsWhenAgentHasNoKnowledge(t *testing.T) {
|
|
||||||
decision := buildKnowledgeUnavailableDecision(newKnowledgeGuardAgentFixture(), nil)
|
|
||||||
|
|
||||||
if decision.FallbackReply != "" {
|
|
||||||
t.Fatalf("expected no fallback without knowledge bases, got %q", decision.FallbackReply)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveKnowledgeHumanSupportFallbackUsesAgentMessage(t *testing.T) {
|
|
||||||
agent := newKnowledgeGuardAgentFixture()
|
|
||||||
agent.FallbackMessage = "我暂时没有找到足够准确的信息。"
|
|
||||||
|
|
||||||
got := resolveKnowledgeHumanSupportFallback(agent)
|
|
||||||
|
|
||||||
want := "我暂时没有找到足够准确的信息。 建议你联系人工客服进一步确认。"
|
|
||||||
if got != want {
|
|
||||||
t.Fatalf("unexpected fallback: %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveKnowledgeHumanSupportFallbackUsesDefault(t *testing.T) {
|
|
||||||
agent := newKnowledgeGuardAgentFixture()
|
|
||||||
|
|
||||||
got := resolveKnowledgeHumanSupportFallback(agent)
|
|
||||||
|
|
||||||
want := "当前知识库暂无明确信息。 建议你联系人工客服进一步确认。"
|
|
||||||
if got != want {
|
|
||||||
t.Fatalf("unexpected fallback: %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newKnowledgeGuardAgentFixture() models.AIAgent {
|
|
||||||
return models.AIAgent{
|
|
||||||
FallbackMode: enums.AIAgentFallbackModeNoAnswer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mlogclub/simple/sqls"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,16 +22,11 @@ type ticketNoSequenceService struct {
|
|||||||
ticketNoSQLiteMu sync.Mutex
|
ticketNoSQLiteMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ticketNoSequenceService) Next(tx *gorm.DB, now time.Time) (string, error) {
|
func (s *ticketNoSequenceService) Next(now time.Time) (string, error) {
|
||||||
if tx == nil {
|
s.ticketNoSQLiteMu.Lock()
|
||||||
return "", fmt.Errorf("ticket number transaction is required")
|
defer s.ticketNoSQLiteMu.Unlock()
|
||||||
}
|
|
||||||
if tx.Dialector.Name() == "sqlite" {
|
return s.nextWithRetry(sqls.DB(), now)
|
||||||
s.ticketNoSQLiteMu.Lock()
|
|
||||||
defer s.ticketNoSQLiteMu.Unlock()
|
|
||||||
return s.nextWithRetry(tx, now)
|
|
||||||
}
|
|
||||||
return s.nextWithRetry(tx, now)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ticketNoSequenceService) nextWithRetry(tx *gorm.DB, now time.Time) (string, error) {
|
func (s *ticketNoSequenceService) nextWithRetry(tx *gorm.DB, now time.Time) (string, error) {
|
||||||
|
|||||||
@@ -183,7 +183,6 @@ func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator *
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
ticket := &models.Ticket{
|
ticket := &models.Ticket{
|
||||||
Title: title,
|
Title: title,
|
||||||
Description: description,
|
Description: description,
|
||||||
@@ -195,27 +194,25 @@ func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator *
|
|||||||
CurrentAssigneeID: req.CurrentAssigneeID,
|
CurrentAssigneeID: req.CurrentAssigneeID,
|
||||||
AuditFields: utils.BuildAuditFields(operator),
|
AuditFields: utils.BuildAuditFields(operator),
|
||||||
}
|
}
|
||||||
ticket.UpdatedAt = now
|
|
||||||
|
|
||||||
if err := withSQLiteTicketCreateLock(sqls.DB(), func() error {
|
ticketNo, err := TicketNoSequenceService.Next(ticket.CreatedAt)
|
||||||
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
if err != nil {
|
||||||
ticketNo, err := TicketNoSequenceService.nextWithRetry(ctx.Tx, now)
|
return nil, err
|
||||||
if err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||||
ticket.TicketNo = ticketNo
|
ticket.TicketNo = ticketNo
|
||||||
if err := repositories.TicketRepository.Create(ctx.Tx, ticket); err != nil {
|
if err := repositories.TicketRepository.Create(ctx.Tx, ticket); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := TicketTagService.ReplaceTicketTags(ctx.Tx, ticket.ID, tagIDs, operator); err != nil {
|
if err := TicketTagService.ReplaceTicketTags(ctx.Tx, ticket.ID, tagIDs, operator); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return repositories.TicketProgressRepository.Create(ctx.Tx, &models.TicketProgress{
|
return repositories.TicketProgressRepository.Create(ctx.Tx, &models.TicketProgress{
|
||||||
TicketID: ticket.ID,
|
TicketID: ticket.ID,
|
||||||
Content: "创建工单",
|
Content: "创建工单",
|
||||||
AuthorID: operator.UserID,
|
AuthorID: operator.UserID,
|
||||||
CreatedAt: now,
|
CreatedAt: time.Now(),
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -228,14 +225,6 @@ func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator *
|
|||||||
return s.Get(ticket.ID), nil
|
return s.Get(ticket.ID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func withSQLiteTicketCreateLock(db *gorm.DB, fn func() error) error {
|
|
||||||
if db != nil && db.Dialector.Name() == "sqlite" {
|
|
||||||
TicketNoSequenceService.ticketNoSQLiteMu.Lock()
|
|
||||||
defer TicketNoSequenceService.ticketNoSQLiteMu.Unlock()
|
|
||||||
}
|
|
||||||
return fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConversationRequest, operator *dto.AuthPrincipal) (*models.Ticket, error) {
|
func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConversationRequest, operator *dto.AuthPrincipal) (*models.Ticket, error) {
|
||||||
if operator == nil {
|
if operator == nil {
|
||||||
return nil, errorsx.Unauthorized("未登录或登录已过期")
|
return nil, errorsx.Unauthorized("未登录或登录已过期")
|
||||||
|
|||||||
@@ -424,21 +424,15 @@ func TestTicketServiceTicketNoNextConcurrent(t *testing.T) {
|
|||||||
errs := make(chan error, count)
|
errs := make(chan error, count)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
for range count {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
ticketNo, err := services.TicketNoSequenceService.Next(time.Now())
|
||||||
ticketNo, err := services.TicketNoSequenceService.Next(ctx.Tx, time.Now())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
results <- ticketNo
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs <- err
|
errs <- err
|
||||||
}
|
}
|
||||||
|
results <- ticketNo
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user