Files
agent-desk-ng/internal/services/agent_presence_service.go
mono 2f7a491666 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.
2026-10-16 15:21:42 +00:00

78 lines
2.4 KiB
Go

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