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.
531 lines
22 KiB
Go
531 lines
22 KiB
Go
// Modified in AgentDeskNext: authenticated support availability and calendars.
|
||
package services
|
||
|
||
import (
|
||
"agent-desk/internal/models"
|
||
"agent-desk/internal/pkg/dto/response"
|
||
"agent-desk/internal/pkg/enums"
|
||
"agent-desk/internal/pkg/i18nx"
|
||
"agent-desk/internal/repositories"
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/mlogclub/simple/sqls"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
var DashboardService = newDashboardService()
|
||
|
||
func newDashboardService() *dashboardService {
|
||
return &dashboardService{}
|
||
}
|
||
|
||
type dashboardService struct {
|
||
}
|
||
|
||
func (s *dashboardService) GetOverview(rangeValue string, locale string) response.DashboardOverviewResponse {
|
||
locale = i18nx.NormalizeLocale(locale)
|
||
now := time.Now()
|
||
normalizedRange, trendDays := normalizeDashboardRange(rangeValue)
|
||
todayStart := startOfDay(now)
|
||
trendStart := todayStart.AddDate(0, 0, -(trendDays - 1))
|
||
db := sqls.DB()
|
||
|
||
conversationTodayCount := repositories.DashboardRepository.CountConversations(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("created_at >= ?", todayStart)
|
||
})
|
||
processingConversationCount := repositories.DashboardRepository.CountConversations(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("status IN ?", []enums.IMConversationStatus{
|
||
enums.IMConversationStatusAIServing,
|
||
enums.IMConversationStatusActive,
|
||
})
|
||
})
|
||
pendingConversationCount := repositories.DashboardRepository.CountConversations(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("status = ?", enums.IMConversationStatusPending)
|
||
})
|
||
|
||
agentProfiles := repositories.DashboardRepository.ListEnabledAgentProfiles(db)
|
||
agentTeams := repositories.DashboardRepository.ListEnabledAgentTeams(db)
|
||
activeSchedules := repositories.DashboardRepository.ListActiveTeamSchedules(db, now, now)
|
||
activeConversations := repositories.DashboardRepository.ListConversations(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("status IN ?", []enums.IMConversationStatus{
|
||
enums.IMConversationStatusAIServing,
|
||
enums.IMConversationStatusPending,
|
||
enums.IMConversationStatusActive,
|
||
})
|
||
})
|
||
|
||
onlineAgents, busyAgents, offlineAgents, teamLoads := s.buildAgentStats(now, agentTeams, agentProfiles, activeSchedules, activeConversations)
|
||
|
||
enabledAIAgentCount := repositories.DashboardRepository.CountAIAgents(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("status = ?", enums.StatusOk)
|
||
})
|
||
enabledChannelCount := repositories.DashboardRepository.CountChannels(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("status = ?", enums.StatusOk)
|
||
})
|
||
knowledgeRetrieveCount := repositories.DashboardRepository.CountKnowledgeRetrieveLogs(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("created_at >= ?", todayStart)
|
||
})
|
||
knowledgeRetrieveFailCount := repositories.DashboardRepository.CountKnowledgeRetrieveLogs(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("created_at >= ? AND answer_status IN ?", todayStart, []int{2, 3, 4})
|
||
})
|
||
agentRunFailCount := repositories.DashboardRepository.CountAgentRuns(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("created_at >= ? AND status = ?", todayStart, "failed")
|
||
})
|
||
aiHandoffCount := repositories.DashboardRepository.CountConversations(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("handoff_at >= ?", todayStart)
|
||
})
|
||
|
||
enabledAIAgents := repositories.DashboardRepository.ListAIAgents(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("status = ?", enums.StatusOk)
|
||
})
|
||
alerts := s.buildAlerts(now, db, enabledAIAgents, agentTeams, activeSchedules, locale)
|
||
|
||
return response.DashboardOverviewResponse{
|
||
Range: normalizedRange,
|
||
GeneratedAt: now.Format("2006-01-02 15:04:05"),
|
||
Summary: response.DashboardSummaryResponse{
|
||
TodayNewConversations: conversationTodayCount,
|
||
ProcessingConversations: processingConversationCount,
|
||
PendingDispatchConversations: pendingConversationCount,
|
||
OnlineAgents: onlineAgents,
|
||
AIServiceRate: calcAIServiceRate(activeConversations),
|
||
},
|
||
ConversationStats: response.DashboardSectionStatsResponse{
|
||
StatusDistribution: buildConversationStatusDistribution(db, locale),
|
||
Trend: buildConversationTrend(db, trendStart),
|
||
},
|
||
AgentStats: response.DashboardAgentStatsResponse{
|
||
OnlineAgents: onlineAgents,
|
||
BusyAgents: busyAgents,
|
||
OfflineAgents: offlineAgents,
|
||
TeamLoads: teamLoads,
|
||
},
|
||
AIStats: response.DashboardAIStatsResponse{
|
||
EnabledAIAgents: enabledAIAgentCount,
|
||
EnabledChannels: enabledChannelCount,
|
||
TodayKnowledgeRetrieves: knowledgeRetrieveCount,
|
||
TodayKnowledgeRetrieveFailCount: knowledgeRetrieveFailCount,
|
||
TodayKnowledgeRetrieveFailRate: calcRate(knowledgeRetrieveFailCount, knowledgeRetrieveCount),
|
||
TodayAgentRunFailCount: agentRunFailCount,
|
||
TodayAIHandoffCount: aiHandoffCount,
|
||
},
|
||
Alerts: alerts,
|
||
QuickLinks: buildDashboardQuickLinks(locale),
|
||
}
|
||
}
|
||
|
||
func (s *dashboardService) buildAgentStats(now time.Time, teams []models.AgentTeam, profiles []models.AgentProfile, schedules []models.AgentTeamSchedule, conversations []models.Conversation) (int64, int64, int64, []response.DashboardTeamLoadResponse) {
|
||
userIDs := make([]int64, 0, len(profiles))
|
||
for _, profile := range profiles {
|
||
userIDs = append(userIDs, profile.UserID)
|
||
}
|
||
online, _ := SupportAvailabilityService.onlineUsers(sqls.DB(), userIDs, now)
|
||
|
||
scheduledTeamIDs := make(map[int64]bool, len(schedules))
|
||
for _, team := range teams {
|
||
calendar, err := SupportCalendarService.Evaluate(sqls.DB(), team.ID, now)
|
||
if err == nil {
|
||
scheduledTeamIDs[team.ID] = calendar.Open
|
||
}
|
||
}
|
||
|
||
type teamCounter struct {
|
||
totalAgents int64
|
||
onlineAgents int64
|
||
busyAgents int64
|
||
offlineAgents int64
|
||
waitingConversations int64
|
||
processingConversations int64
|
||
maxConcurrentCapacity int64
|
||
}
|
||
|
||
teamCounters := make(map[int64]*teamCounter, len(teams))
|
||
for _, team := range teams {
|
||
teamCounters[team.ID] = &teamCounter{}
|
||
}
|
||
|
||
var onlineAgents int64
|
||
var busyAgents int64
|
||
var offlineAgents int64
|
||
|
||
for _, profile := range profiles {
|
||
counter := teamCounters[profile.TeamID]
|
||
if counter == nil {
|
||
counter = &teamCounter{}
|
||
teamCounters[profile.TeamID] = counter
|
||
}
|
||
counter.totalAgents++
|
||
counter.maxConcurrentCapacity += int64(profile.MaxConcurrentCount)
|
||
if online[profile.UserID] {
|
||
counter.onlineAgents++
|
||
onlineAgents++
|
||
if effectiveSupportStatus(profile, now) == enums.ServiceStatusBusy {
|
||
counter.busyAgents++
|
||
busyAgents++
|
||
}
|
||
continue
|
||
}
|
||
counter.offlineAgents++
|
||
offlineAgents++
|
||
}
|
||
|
||
for _, item := range conversations {
|
||
if item.CurrentTeamID <= 0 {
|
||
continue
|
||
}
|
||
counter := teamCounters[item.CurrentTeamID]
|
||
if counter == nil {
|
||
counter = &teamCounter{}
|
||
teamCounters[item.CurrentTeamID] = counter
|
||
}
|
||
switch item.Status {
|
||
case enums.IMConversationStatusAIServing:
|
||
counter.processingConversations++
|
||
case enums.IMConversationStatusPending:
|
||
counter.waitingConversations++
|
||
case enums.IMConversationStatusActive:
|
||
counter.processingConversations++
|
||
}
|
||
}
|
||
|
||
teamLoads := make([]response.DashboardTeamLoadResponse, 0, len(teams))
|
||
for _, team := range teams {
|
||
counter := teamCounters[team.ID]
|
||
if counter == nil {
|
||
counter = &teamCounter{}
|
||
}
|
||
teamLoads = append(teamLoads, response.DashboardTeamLoadResponse{
|
||
TeamID: team.ID,
|
||
TeamName: team.Name,
|
||
TotalAgents: counter.totalAgents,
|
||
OnlineAgents: counter.onlineAgents,
|
||
BusyAgents: counter.busyAgents,
|
||
OfflineAgents: counter.offlineAgents,
|
||
WaitingConversations: counter.waitingConversations,
|
||
ProcessingConversations: counter.processingConversations,
|
||
MaxConcurrentCapacity: counter.maxConcurrentCapacity,
|
||
LoadRate: calcRate(counter.processingConversations, counter.maxConcurrentCapacity),
|
||
HasScheduleNow: scheduledTeamIDs[team.ID],
|
||
})
|
||
}
|
||
|
||
sort.Slice(teamLoads, func(i, j int) bool {
|
||
if teamLoads[i].WaitingConversations == teamLoads[j].WaitingConversations {
|
||
if teamLoads[i].LoadRate == teamLoads[j].LoadRate {
|
||
return teamLoads[i].TeamID < teamLoads[j].TeamID
|
||
}
|
||
return teamLoads[i].LoadRate > teamLoads[j].LoadRate
|
||
}
|
||
return teamLoads[i].WaitingConversations > teamLoads[j].WaitingConversations
|
||
})
|
||
|
||
return onlineAgents, busyAgents, offlineAgents, teamLoads
|
||
}
|
||
|
||
func (s *dashboardService) buildAlerts(now time.Time, db *gorm.DB, aiAgents []models.AIAgent, teams []models.AgentTeam, schedules []models.AgentTeamSchedule, locale string) []response.DashboardAlertResponse {
|
||
alerts := make([]response.DashboardAlertResponse, 0, 4)
|
||
pendingTimeout := now.Add(-10 * time.Minute)
|
||
activeTimeout := now.Add(-30 * time.Minute)
|
||
|
||
pendingLongWaitCount := repositories.DashboardRepository.CountConversations(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("status = ? AND created_at < ?", enums.IMConversationStatusPending, pendingTimeout)
|
||
})
|
||
if pendingLongWaitCount > 0 {
|
||
alerts = append(alerts, response.DashboardAlertResponse{
|
||
ID: "pending-long-wait",
|
||
Level: "warning",
|
||
Title: dashboardText(locale, "alert.pendingLongWait.title"),
|
||
Description: dashboardText(locale, "alert.pendingLongWait.description"),
|
||
Count: pendingLongWaitCount,
|
||
Link: "/dashboard/conversations",
|
||
})
|
||
}
|
||
|
||
staleProcessingCount := repositories.DashboardRepository.CountConversations(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("status IN ? AND (last_message_at IS NULL OR last_message_at < ?)", []enums.IMConversationStatus{
|
||
enums.IMConversationStatusAIServing,
|
||
enums.IMConversationStatusActive,
|
||
}, activeTimeout)
|
||
})
|
||
if staleProcessingCount > 0 {
|
||
alerts = append(alerts, response.DashboardAlertResponse{
|
||
ID: "stale-processing",
|
||
Level: "warning",
|
||
Title: dashboardText(locale, "alert.staleProcessing.title"),
|
||
Description: dashboardText(locale, "alert.staleProcessing.description"),
|
||
Count: staleProcessingCount,
|
||
Link: "/dashboard/conversations",
|
||
})
|
||
}
|
||
|
||
scheduledTeamIDs := make(map[int64]bool, len(schedules))
|
||
for _, team := range teams {
|
||
calendar, err := SupportCalendarService.Evaluate(sqls.DB(), team.ID, now)
|
||
if err == nil {
|
||
scheduledTeamIDs[team.ID] = calendar.Open
|
||
}
|
||
}
|
||
var scheduleMissingCount int64
|
||
for _, team := range teams {
|
||
if !scheduledTeamIDs[team.ID] {
|
||
scheduleMissingCount++
|
||
}
|
||
}
|
||
if scheduleMissingCount > 0 {
|
||
alerts = append(alerts, response.DashboardAlertResponse{
|
||
ID: "team-no-schedule",
|
||
Level: "info",
|
||
Title: dashboardText(locale, "alert.teamNoSchedule.title"),
|
||
Description: dashboardText(locale, "alert.teamNoSchedule.description"),
|
||
Count: scheduleMissingCount,
|
||
Link: "/dashboard/agent-team-schedules",
|
||
})
|
||
}
|
||
|
||
sort.Slice(alerts, func(i, j int) bool {
|
||
if alerts[i].Count == alerts[j].Count {
|
||
return alerts[i].ID < alerts[j].ID
|
||
}
|
||
return alerts[i].Count > alerts[j].Count
|
||
})
|
||
|
||
return alerts
|
||
}
|
||
|
||
func buildConversationStatusDistribution(db *gorm.DB, locale string) []response.DashboardStatusDistributionItem {
|
||
ret := make([]response.DashboardStatusDistributionItem, 0, len(enums.IMConversationStatusValues))
|
||
for _, status := range enums.IMConversationStatusValues {
|
||
ret = append(ret, response.DashboardStatusDistributionItem{
|
||
Status: int(status),
|
||
Label: conversationStatusLabel(status, locale),
|
||
Count: repositories.DashboardRepository.CountConversations(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Where("status = ?", status)
|
||
}),
|
||
})
|
||
}
|
||
return ret
|
||
}
|
||
|
||
func buildConversationTrend(db *gorm.DB, start time.Time) []response.DashboardTrendItem {
|
||
created := repositories.DashboardRepository.ListConversations(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Select("created_at").Where("created_at >= ?", start)
|
||
})
|
||
closed := repositories.DashboardRepository.ListConversations(db, func(tx *gorm.DB) *gorm.DB {
|
||
return tx.Select("closed_at").Where("closed_at IS NOT NULL AND closed_at >= ?", start)
|
||
})
|
||
return buildTrendItems(start, created, closed, func(item models.Conversation) *time.Time {
|
||
return &item.CreatedAt
|
||
}, func(item models.Conversation) *time.Time {
|
||
return item.ClosedAt
|
||
})
|
||
}
|
||
|
||
func buildTrendItems(start time.Time, created []models.Conversation, closed []models.Conversation, createdAt func(models.Conversation) *time.Time, closedAt func(models.Conversation) *time.Time) []response.DashboardTrendItem {
|
||
series := initTrendMap(start, time.Now())
|
||
for _, item := range created {
|
||
if ts := createdAt(item); ts != nil {
|
||
series[ts.Format("2006-01-02")].NewCount++
|
||
}
|
||
}
|
||
for _, item := range closed {
|
||
if ts := closedAt(item); ts != nil {
|
||
series[ts.Format("2006-01-02")].ClosedCount++
|
||
}
|
||
}
|
||
return flattenTrendMap(series)
|
||
}
|
||
|
||
func initTrendMap(start, end time.Time) map[string]*response.DashboardTrendItem {
|
||
series := make(map[string]*response.DashboardTrendItem)
|
||
for current := startOfDay(start); !current.After(end); current = current.AddDate(0, 0, 1) {
|
||
key := current.Format("2006-01-02")
|
||
series[key] = &response.DashboardTrendItem{Date: key}
|
||
}
|
||
return series
|
||
}
|
||
|
||
func flattenTrendMap(series map[string]*response.DashboardTrendItem) []response.DashboardTrendItem {
|
||
keys := make([]string, 0, len(series))
|
||
for key := range series {
|
||
keys = append(keys, key)
|
||
}
|
||
sort.Strings(keys)
|
||
ret := make([]response.DashboardTrendItem, 0, len(keys))
|
||
for _, key := range keys {
|
||
ret = append(ret, *series[key])
|
||
}
|
||
return ret
|
||
}
|
||
|
||
func buildDashboardQuickLinks(locale string) []response.DashboardQuickLinkResponse {
|
||
return []response.DashboardQuickLinkResponse{
|
||
{Title: dashboardText(locale, "quick.conversations.title"), Description: dashboardText(locale, "quick.conversations.description"), Link: "/dashboard/conversations"},
|
||
{Title: dashboardText(locale, "quick.agents.title"), Description: dashboardText(locale, "quick.agents.description"), Link: "/dashboard/agents"},
|
||
{Title: dashboardText(locale, "quick.knowledge.title"), Description: dashboardText(locale, "quick.knowledge.description"), Link: "/dashboard/knowledge"},
|
||
{Title: dashboardText(locale, "quick.aiAgents.title"), Description: dashboardText(locale, "quick.aiAgents.description"), Link: "/dashboard/ai-agents"},
|
||
{Title: dashboardText(locale, "quick.channels.title"), Description: dashboardText(locale, "quick.channels.description"), Link: "/dashboard/channels"},
|
||
}
|
||
}
|
||
|
||
func normalizeDashboardRange(value string) (string, int) {
|
||
switch value {
|
||
case "30d":
|
||
return "30d", 30
|
||
case "today":
|
||
return "today", 1
|
||
default:
|
||
return "7d", 7
|
||
}
|
||
}
|
||
|
||
func startOfDay(value time.Time) time.Time {
|
||
return time.Date(value.Year(), value.Month(), value.Day(), 0, 0, 0, 0, value.Location())
|
||
}
|
||
|
||
func calcRate(numerator, denominator int64) float64 {
|
||
if denominator <= 0 {
|
||
return 0
|
||
}
|
||
ratio := float64(numerator) / float64(denominator) * 100
|
||
return float64(int(ratio*10+0.5)) / 10
|
||
}
|
||
|
||
func calcAIServiceRate(conversations []models.Conversation) float64 {
|
||
var aiCount int64
|
||
var total int64
|
||
for _, item := range conversations {
|
||
total++
|
||
if item.ServiceMode == enums.IMConversationServiceModeAIOnly || item.ServiceMode == enums.IMConversationServiceModeAIFirst {
|
||
aiCount++
|
||
}
|
||
}
|
||
return calcRate(aiCount, total)
|
||
}
|
||
|
||
func labelOrDefault(value, fallback string) string {
|
||
if strings.TrimSpace(value) != "" {
|
||
return value
|
||
}
|
||
return fallback
|
||
}
|
||
|
||
func conversationStatusLabel(status enums.IMConversationStatus, locale string) string {
|
||
normalized := i18nx.NormalizeLocale(locale)
|
||
if normalized == i18nx.LocaleRuRU {
|
||
switch status {
|
||
case enums.IMConversationStatusAIServing:
|
||
return "Обрабатывает ИИ"
|
||
case enums.IMConversationStatusPending:
|
||
return "В очереди"
|
||
case enums.IMConversationStatusActive:
|
||
return "В работе"
|
||
case enums.IMConversationStatusClosed:
|
||
return "Закрыт"
|
||
default:
|
||
return fmt.Sprintf("Статус %d", status)
|
||
}
|
||
}
|
||
if normalized == i18nx.LocaleZhCN {
|
||
return labelOrDefault(enums.GetIMConversationStatusLabel(status), fmt.Sprintf("状态 %d", status))
|
||
}
|
||
switch status {
|
||
case enums.IMConversationStatusAIServing:
|
||
return "AI active"
|
||
case enums.IMConversationStatusPending:
|
||
return "Queued"
|
||
case enums.IMConversationStatusActive:
|
||
return "In progress"
|
||
case enums.IMConversationStatusClosed:
|
||
return "Closed"
|
||
default:
|
||
return fmt.Sprintf("Status %d", status)
|
||
}
|
||
}
|
||
|
||
func dashboardText(locale string, key string) string {
|
||
normalized := i18nx.NormalizeLocale(locale)
|
||
if normalized == i18nx.LocaleRuRU {
|
||
if value, ok := dashboardRuRU[key]; ok {
|
||
return value
|
||
}
|
||
}
|
||
if normalized == i18nx.LocaleZhCN {
|
||
if value, ok := dashboardZhCN[key]; ok {
|
||
return value
|
||
}
|
||
}
|
||
if value, ok := dashboardEnUS[key]; ok {
|
||
return value
|
||
}
|
||
if value, ok := dashboardZhCN[key]; ok {
|
||
return value
|
||
}
|
||
return key
|
||
}
|
||
|
||
var dashboardRuRU = map[string]string{
|
||
"alert.pendingLongWait.title": "Очередь диалогов переполнена",
|
||
"alert.pendingLongWait.description": "Некоторые диалоги ожидают назначения более 10 минут. Рекомендуется распределить обращения.",
|
||
"alert.staleProcessing.title": "Диалоги в работе требуют внимания",
|
||
"alert.staleProcessing.description": "В некоторых активных диалогах нет новых сообщений более 30 минут. Проверьте их статус.",
|
||
"alert.teamNoSchedule.title": "У команд операторов нет активного расписания",
|
||
"alert.teamNoSchedule.description": "У части активных команд операторов сейчас нет действующего расписания, что может повлиять на автоназначение.",
|
||
"alert.aiNoKnowledge.title": "ИИ-агенты не привязаны к базе знаний",
|
||
"alert.aiNoKnowledge.description": "Некоторые активные ИИ-агенты не привязаны к базе знаний, что может снизить качество ответов.",
|
||
"quick.conversations.title": "Диалоги",
|
||
"quick.conversations.description": "Просмотр очереди и активных диалогов",
|
||
"quick.agents.title": "Операторы",
|
||
"quick.agents.description": "Статус операторов и настройка команд",
|
||
"quick.knowledge.title": "База знаний",
|
||
"quick.knowledge.description": "Управление документами и журнал поиска",
|
||
"quick.aiAgents.title": "ИИ-агенты",
|
||
"quick.aiAgents.description": "Настройка стратегий приёма и базы знаний",
|
||
"quick.channels.title": "Каналы связи",
|
||
"quick.channels.description": "Управление каналами и агентами по умолчанию",
|
||
}
|
||
|
||
var dashboardZhCN = map[string]string{
|
||
"alert.pendingLongWait.title": "待接入会话堆积",
|
||
"alert.pendingLongWait.description": "存在超过 10 分钟仍未接入的会话,建议优先处理分配。",
|
||
"alert.staleProcessing.title": "处理中会话长时间无响应",
|
||
"alert.staleProcessing.description": "部分处理中会话已超过 30 分钟没有最新消息,需要确认跟进状态。",
|
||
"alert.teamNoSchedule.title": "客服组当前无生效排班",
|
||
"alert.teamNoSchedule.description": "部分启用中的客服组当前没有生效排班,可能影响自动分配。",
|
||
"alert.aiNoKnowledge.title": "AI Agent 未绑定知识库",
|
||
"alert.aiNoKnowledge.description": "部分启用中的 AI Agent 尚未绑定知识库,回答质量可能不稳定。",
|
||
"quick.conversations.title": "会话管理",
|
||
"quick.conversations.description": "查看待接入与处理中会话",
|
||
"quick.agents.title": "客服档案",
|
||
"quick.agents.description": "查看客服状态与分组配置",
|
||
"quick.knowledge.title": "知识库",
|
||
"quick.knowledge.description": "维护文档与查看检索日志",
|
||
"quick.aiAgents.title": "AI Agent",
|
||
"quick.aiAgents.description": "配置 AI 接待策略与知识绑定",
|
||
"quick.channels.title": "接入渠道",
|
||
"quick.channels.description": "管理接入渠道与默认 Agent",
|
||
}
|
||
|
||
var dashboardEnUS = map[string]string{
|
||
"alert.pendingLongWait.title": "Queued conversations are piling up",
|
||
"alert.pendingLongWait.description": "Some conversations have been waiting for more than 10 minutes. Prioritize assignment.",
|
||
"alert.staleProcessing.title": "Active conversations need attention",
|
||
"alert.staleProcessing.description": "Some active conversations have had no new messages for over 30 minutes. Check their follow-up status.",
|
||
"alert.teamNoSchedule.title": "Agent teams have no active schedule",
|
||
"alert.teamNoSchedule.description": "Some enabled agent teams do not have an active schedule right now, which may affect automatic assignment.",
|
||
"alert.aiNoKnowledge.title": "AI Agents are missing knowledge bases",
|
||
"alert.aiNoKnowledge.description": "Some enabled AI Agents are not linked to a knowledge base yet, which may reduce answer quality.",
|
||
"quick.conversations.title": "Conversations",
|
||
"quick.conversations.description": "Review queued and active conversations",
|
||
"quick.agents.title": "Agents",
|
||
"quick.agents.description": "Check agent status and team setup",
|
||
"quick.knowledge.title": "Knowledge base",
|
||
"quick.knowledge.description": "Manage documents and review retrieval logs",
|
||
"quick.aiAgents.title": "AI Agents",
|
||
"quick.aiAgents.description": "Configure AI service policies and knowledge bindings",
|
||
"quick.channels.title": "Channels",
|
||
"quick.channels.description": "Manage channels and default agents",
|
||
}
|