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:
2026-10-16 15:21:42 +00:00
parent 299ea50fb5
commit 2f7a491666
176 changed files with 11869 additions and 2716 deletions

View File

@@ -1,3 +1,6 @@
// Modified in AgentDeskNext: authenticated support availability and calendars.
// Modified in the independent hardening fork; see docs/hardening.
package services
import (
@@ -6,8 +9,11 @@ import (
"testing"
"time"
"agent-desk/internal/events"
"agent-desk/internal/models"
"agent-desk/internal/pkg/constants"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/eventbus"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
@@ -112,11 +118,13 @@ func findHumanDispatchRealtimeEvent(t *testing.T, session *ClientSession, eventT
func captureHumanDispatchRealtimeSession(t *testing.T, topics ...string) *ClientSession {
t.Helper()
principal := seedRealtimeStaffPrincipal(t, sqls.DB())
session := &ClientSession{
ID: "test-session",
Role: realtimeRoleAdmin,
Topics: map[string]struct{}{},
Send: make(chan []byte, 32),
ID: "test-session",
Role: realtimeRoleAdmin,
Principal: principal,
Topics: map[string]struct{}{},
Send: make(chan []byte, 32),
}
WsService.manager.Register(session, topics)
return session
@@ -124,6 +132,7 @@ func captureHumanDispatchRealtimeSession(t *testing.T, topics ...string) *Client
func setupHumanDispatchRealtimeTestDB(t *testing.T) *gorm.DB {
t.Helper()
previousDB, previousWs := sqls.DB(), WsService
dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory&cache=shared"), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
@@ -150,6 +159,7 @@ func setupHumanDispatchRealtimeTestDB(t *testing.T) *gorm.DB {
&models.AgentTeam{},
&models.AgentTeamSchedule{},
&models.AgentProfile{},
&models.SupportCalendar{}, &models.AgentPresenceLease{}, &models.LoginSession{}, &models.Role{}, &models.Permission{}, &models.UserRole{}, &models.RolePermission{}, &models.UserPermission{},
&models.Conversation{},
&models.ConversationParticipant{},
&models.ConversationAssignment{},
@@ -161,6 +171,13 @@ func setupHumanDispatchRealtimeTestDB(t *testing.T) *gorm.DB {
t.Fatalf("auto migrate error = %v", err)
}
sqls.SetDB(db)
t.Cleanup(func() {
// Assignment notifications run asynchronously. Join them before replacing
// the global fixture DB/service or closing its SQLite connection.
eventbus.Get[events.ConversationAssignedEvent]().Wait()
sqls.SetDB(previousDB)
WsService = previousWs
})
return db
}
@@ -205,6 +222,7 @@ func createHumanDispatchRealtimeAgentProfile(t *testing.T, db *gorm.DB, userID,
Username: "agent",
Nickname: "客服",
Status: enums.StatusOk,
UserType: enums.UserTypeEmployee,
}).Error; err != nil {
t.Fatalf("create user error = %v", err)
}
@@ -220,6 +238,25 @@ func createHumanDispatchRealtimeAgentProfile(t *testing.T, db *gorm.DB, userID,
}).Error; err != nil {
t.Fatalf("create profile error = %v", err)
}
role := models.Role{Code: "test-live-agent", Status: enums.StatusOk}
permission := models.Permission{Code: constants.PermissionConversationView.Code, Status: enums.StatusOk}
session := models.LoginSession{UserID: userID, Token: "ak_isolated_routing_fixture", ExpiredAt: time.Now().Add(time.Hour)}
// The observing WebSocket already seeds this shared permission. Reuse it
// without changing its state, while the assigned operator gets its own role.
if err := db.Where("code = ?", permission.Code).FirstOrCreate(&permission).Error; err != nil {
t.Fatal(err)
}
for _, item := range []any{&role, &session} {
if err := db.Create(item).Error; err != nil {
t.Fatal(err)
}
}
for _, item := range []any{&models.UserRole{UserID: userID, RoleID: role.ID}, &models.RolePermission{RoleID: role.ID, PermissionID: permission.ID}, &models.AgentPresenceLease{ID: "isolated-routing-presence", UserID: userID, LoginSessionID: session.ID, ExpiresAt: time.Now().Add(time.Minute)}} {
if err := db.Create(item).Error; err != nil {
t.Fatal(err)
}
}
}
func createHumanDispatchRealtimeConversation(t *testing.T, db *gorm.DB, aiAgentID int64) models.Conversation {