feat: harden support, localize knowledge and add operator availability [skip ci]
Source-only publication of reviewed commit 64e96a7f423673dc29ad56485be9fc79bce8af1c.
Passed in isolation: focused frontend/backend tests, race checks, TypeScript typecheck, lint, Go compilation, Flowgram build, and deterministic SDK build/tests.
Not accepted as a production release: Next.js export/native assembly remains blocked by environment memory limits; browser interaction and MySQL staging acceptance remain unverified. No production server, database/schema, runtime configuration, deployment, release tag, or workflow execution was performed.
Expected source tree: 0a4a7528e9.
This commit is contained in:
77
internal/services/agent_presence_service.go
Normal file
77
internal/services/agent_presence_service.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/constants"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/repositories"
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
const agentPresenceLeaseTTL = 90 * time.Second
|
||||
const agentPresenceDisconnectGrace = 15 * time.Second
|
||||
const agentPresenceRenewInterval = 15 * time.Second
|
||||
|
||||
func (s *wsService) renewAgentPresence(session *ClientSession) {
|
||||
if session == nil || session.Closed.Load() || session.Principal == nil || (session.Role != realtimeRoleAdmin && session.Role != realtimeRoleNotification) {
|
||||
return
|
||||
}
|
||||
session.presenceMu.Lock()
|
||||
dispatchPending := false
|
||||
defer func() {
|
||||
session.presenceMu.Unlock()
|
||||
// Finish the presence write before dispatching, and keep ownership in
|
||||
// this call rather than launching an untracked database goroutine.
|
||||
if dispatchPending {
|
||||
_, _ = ConversationDispatchService.DispatchPendingConversations(0)
|
||||
}
|
||||
}()
|
||||
if session.PresenceEnded {
|
||||
return
|
||||
}
|
||||
principal := session.Principal
|
||||
if principal.UserType != enums.UserTypeEmployee || !slices.Contains(principal.Permissions, constants.PermissionConversationView.Code) {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
last := session.PresenceRenewedAt.Load()
|
||||
if now.Unix()-last < int64(agentPresenceRenewInterval/time.Second) {
|
||||
return
|
||||
}
|
||||
if !session.PresenceRenewedAt.CompareAndSwap(last, now.Unix()) {
|
||||
return
|
||||
}
|
||||
// The internal ID is generated independently per connection, across processes.
|
||||
expires := now.Add(agentPresenceLeaseTTL)
|
||||
if principal.ExpiresAt.Before(expires) {
|
||||
expires = principal.ExpiresAt
|
||||
}
|
||||
lease := models.AgentPresenceLease{ID: session.PresenceLeaseID, UserID: principal.UserID, LoginSessionID: principal.LoginSessionID, ExpiresAt: expires}
|
||||
if lease.ID == "" {
|
||||
return
|
||||
}
|
||||
if err := repositories.SupportAvailabilityRepository.RenewPresence(sqls.DB(), &lease); err != nil {
|
||||
slog.Warn("support presence renewal failed")
|
||||
return
|
||||
}
|
||||
if last == 0 {
|
||||
dispatchPending = true
|
||||
}
|
||||
}
|
||||
func (s *wsService) endAgentPresence(session *ClientSession) {
|
||||
if session == nil {
|
||||
return
|
||||
}
|
||||
session.presenceMu.Lock()
|
||||
defer session.presenceMu.Unlock()
|
||||
session.PresenceEnded = true
|
||||
if session.PresenceLeaseID == "" || session.PresenceRenewedAt.Load() == 0 {
|
||||
return
|
||||
}
|
||||
if err := repositories.SupportAvailabilityRepository.EndPresence(sqls.DB(), session.PresenceLeaseID, time.Now().Add(agentPresenceDisconnectGrace)); err != nil {
|
||||
slog.Warn("support presence disconnect update failed")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user