HH-554: connect auto-assignment runtime policy (#135)

* fix(HH-554): connect auto-assignment runtime policy

* fix(HH-554): bound and coalesce assignment jobs

* fix(HH-554): drain assignment backlog safely

* fix(HH-554): hold assignment claim through retries

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-23 22:28:43 +08:00
committed by GitHub
co-authored by rogee
parent 35727be51a
commit 17244bcc9d
15 changed files with 826 additions and 269 deletions
+20 -80
View File
@@ -1,20 +1,6 @@
package autoassignment
// GORM models for the auto-assignment system.
//
// Reference: Chatwoot AutoAssignment pattern
// - AssignmentPolicy: global policy for how conversations are assigned
// (round_robin, longest_waiting). Has fair_distribution_limit and
// fair_distribution_window for rate limiting per agent.
// - InboxAssignmentPolicy: per-inbox override of the global policy.
// Each inbox can have its own assignment policy and rate limits.
//
// Pattern: gochat models embed model.Base (ID, CreatedAt, UpdatedAt, DeletedAt),
// use TableName() method, and use gorm struct tags.
import (
"github.com/gochat/gochat/internal/model"
)
import "github.com/gochat/gochat/internal/model"
// AssignmentPolicyType defines the type of auto-assignment policy.
type AssignmentPolicyType string
@@ -24,79 +10,33 @@ const (
// Reference: Chatwoot InboxRoundRobinService
PolicyRoundRobin AssignmentPolicyType = "round_robin"
// PolicyLongestWaiting assigns conversations to the agent with the
// longest idle time since their last assignment.
// Reference: Chatwoot "longest_waiting" policy (planned feature)
PolicyLongestWaiting AssignmentPolicyType = "longest_waiting"
// PolicyLowestLoad assigns conversations to the agent with the
// fewest currently open conversations.
// Reference: Chatwoot "least_busy" concept — agent with lowest workload.
// This implements Chatwoot's balanced assignment order.
PolicyLowestLoad AssignmentPolicyType = "lowest_load"
)
// AssignmentPolicy represents the global auto-assignment policy for an account.
// Reference: Chatwoot AssignmentPolicy model
// - Defines how unassigned conversations are distributed among agents
// - fair_distribution_limit: max assignments per agent per window (default 5)
// - fair_distribution_window: time window in seconds for rate limiting (default 300 = 5 min)
type AssignmentPolicy struct {
model.Base
AccountID uint `gorm:"index;not null" json:"account_id"`
Policy AssignmentPolicyType `gorm:"size:50;default:round_robin" json:"policy"`
FairDistributionLimit int `gorm:"default:5" json:"fair_distribution_limit"`
FairDistributionWindow int `gorm:"default:300" json:"fair_distribution_window"` // seconds
Active bool `gorm:"default:true" json:"active"`
// EffectivePolicy maps the persisted Chatwoot assignment order to its runtime
// selector. Balanced assignment remains feature-gated like upstream Chatwoot.
func EffectivePolicy(policy *model.AssignmentPolicy, advancedAssignment bool) AssignmentPolicyType {
if policy != nil && policy.AssignmentOrder == 1 && advancedAssignment {
return PolicyLowestLoad
}
return PolicyRoundRobin
}
func (AssignmentPolicy) TableName() string { return "assignment_policies" }
// InboxAssignmentPolicy represents a per-inbox override of the global assignment policy.
// Reference: Chatwoot InboxAssignmentPolicy model
// - Each inbox can override the account-level policy
// - If no inbox-specific policy exists, the account policy is used
type InboxAssignmentPolicy struct {
model.Base
AccountID uint `gorm:"index;not null" json:"account_id"`
InboxID uint `gorm:"uniqueIndex;not null" json:"inbox_id"`
Policy AssignmentPolicyType `gorm:"size:50;default:round_robin" json:"policy"`
FairDistributionLimit int `gorm:"default:5" json:"fair_distribution_limit"`
FairDistributionWindow int `gorm:"default:300" json:"fair_distribution_window"` // seconds
Active bool `gorm:"default:true" json:"active"`
// EffectiveLimit returns the linked policy's fair-distribution limit.
func EffectiveLimit(policy *model.AssignmentPolicy) int {
if policy != nil && policy.FairDistributionLimit > 0 {
return policy.FairDistributionLimit
}
return 5
}
func (InboxAssignmentPolicy) TableName() string { return "inbox_assignment_policies" }
// EffectivePolicy returns the effective assignment policy for a given inbox.
// If the inbox has a specific policy, it is used; otherwise the account policy is used.
func EffectivePolicy(accountPolicy *AssignmentPolicy, inboxPolicy *InboxAssignmentPolicy) AssignmentPolicyType {
if inboxPolicy != nil && inboxPolicy.Active {
return inboxPolicy.Policy
// EffectiveWindow returns the linked policy's rate-limit window in seconds.
func EffectiveWindow(policy *model.AssignmentPolicy) int {
if policy != nil && policy.FairDistributionWindow > 0 {
return policy.FairDistributionWindow
}
if accountPolicy != nil && accountPolicy.Active {
return accountPolicy.Policy
}
return PolicyRoundRobin // default
return 300
}
// EffectiveLimit returns the effective fair distribution limit for a given inbox.
func EffectiveLimit(accountPolicy *AssignmentPolicy, inboxPolicy *InboxAssignmentPolicy) int {
if inboxPolicy != nil && inboxPolicy.Active && inboxPolicy.FairDistributionLimit > 0 {
return inboxPolicy.FairDistributionLimit
}
if accountPolicy != nil && accountPolicy.FairDistributionLimit > 0 {
return accountPolicy.FairDistributionLimit
}
return 5 // default
}
// EffectiveWindow returns the effective fair distribution window (in seconds).
func EffectiveWindow(accountPolicy *AssignmentPolicy, inboxPolicy *InboxAssignmentPolicy) int {
if inboxPolicy != nil && inboxPolicy.Active && inboxPolicy.FairDistributionWindow > 0 {
return inboxPolicy.FairDistributionWindow
}
if accountPolicy != nil && accountPolicy.FairDistributionWindow > 0 {
return accountPolicy.FairDistributionWindow
}
return 300 // default (5 minutes)
}