feat: add AI serving status to conversation management and update related components

This commit is contained in:
mlogclub
2026-04-11 13:55:11 +08:00
parent 97f0df17b6
commit fb4078fc75
13 changed files with 85 additions and 41 deletions
@@ -3,10 +3,11 @@ package request
type AgentConversationFilter string
const (
AgentConversationFilterMine AgentConversationFilter = "mine"
AgentConversationFilterActive AgentConversationFilter = "active"
AgentConversationFilterPending AgentConversationFilter = "pending"
AgentConversationFilterClosed AgentConversationFilter = "closed"
AgentConversationFilterAIServing AgentConversationFilter = "ai_serving"
AgentConversationFilterMine AgentConversationFilter = "mine"
AgentConversationFilterActive AgentConversationFilter = "active"
AgentConversationFilterPending AgentConversationFilter = "pending"
AgentConversationFilterClosed AgentConversationFilter = "closed"
)
type ConversationListRequest struct {
+9 -6
View File
@@ -3,18 +3,21 @@ package enums
type IMConversationStatus int
const (
IMConversationStatusPending IMConversationStatus = 1
IMConversationStatusActive IMConversationStatus = 2
IMConversationStatusClosed IMConversationStatus = 3
IMConversationStatusAIServing IMConversationStatus = 1
IMConversationStatusPending IMConversationStatus = 2
IMConversationStatusActive IMConversationStatus = 3
IMConversationStatusClosed IMConversationStatus = 4
)
var imConversationStatusLabelMap = map[IMConversationStatus]string{
IMConversationStatusPending: "待接入",
IMConversationStatusActive: "处理中",
IMConversationStatusClosed: "已关闭",
IMConversationStatusAIServing: "AI接待中",
IMConversationStatusPending: "待接入",
IMConversationStatusActive: "处理中",
IMConversationStatusClosed: "已关闭",
}
var IMConversationStatusValues = []IMConversationStatus{
IMConversationStatusAIServing,
IMConversationStatusPending,
IMConversationStatusActive,
IMConversationStatusClosed,
+20 -2
View File
@@ -61,6 +61,8 @@ func (s *conversationService) ListConversations(userID int64, filter request.Age
}
switch filter {
case request.AgentConversationFilterAIServing:
cnd.Eq("current_assignee_id", 0).Eq("status", enums.IMConversationStatusAIServing).Desc("last_active_at").Desc("id")
case request.AgentConversationFilterMine:
cnd.Eq("current_assignee_id", userID).Desc("last_active_at").Desc("id")
case request.AgentConversationFilterActive:
@@ -86,6 +88,7 @@ func (s *conversationService) getLatestNotFinished(externalInfo openidentity.Ext
cnd.Eq("external_id", externalInfo.ExternalID)
cnd.Eq("external_source", externalInfo.ExternalSource)
cnd.In("status", []enums.IMConversationStatus{
enums.IMConversationStatusAIServing,
enums.IMConversationStatusPending,
enums.IMConversationStatusActive,
})
@@ -110,7 +113,7 @@ func (s *conversationService) Create(externalInfo openidentity.ExternalInfo, aiA
AIAgentID: aiAgentID,
ExternalSource: externalInfo.ExternalSource,
Subject: subject,
Status: enums.IMConversationStatusPending,
Status: s.resolveInitialStatus(aiAgent.ServiceMode),
ServiceMode: aiAgent.ServiceMode,
Priority: 0,
ExternalID: externalInfo.ExternalID,
@@ -350,7 +353,9 @@ func (s *conversationService) closeConversation(conversationID int64, senderType
if conversation.Status == enums.IMConversationStatusClosed {
return nil
}
if conversation.Status != enums.IMConversationStatusPending && conversation.Status != enums.IMConversationStatusActive {
if conversation.Status != enums.IMConversationStatusAIServing &&
conversation.Status != enums.IMConversationStatusPending &&
conversation.Status != enums.IMConversationStatusActive {
return errorsx.InvalidParam("当前状态不允许关闭会话")
}
var (
@@ -747,6 +752,8 @@ func (s *conversationService) canLinkConversationCustomer(conv *models.Conversat
return true
}
switch conv.Status {
case enums.IMConversationStatusAIServing:
return true
case enums.IMConversationStatusPending:
return true
case enums.IMConversationStatusActive:
@@ -771,3 +778,14 @@ func hashUUID(uuid string) string {
h := md5.Sum([]byte(uuid))
return hex.EncodeToString(h[:])[:8]
}
func (s *conversationService) resolveInitialStatus(serviceMode enums.IMConversationServiceMode) enums.IMConversationStatus {
switch serviceMode {
case enums.IMConversationServiceModeHumanOnly:
return enums.IMConversationStatusPending
case enums.IMConversationServiceModeAIOnly, enums.IMConversationServiceModeAIFirst:
return enums.IMConversationStatusAIServing
default:
return enums.IMConversationStatusAIServing
}
}
+11 -2
View File
@@ -34,7 +34,10 @@ func (s *dashboardService) GetOverview(rangeValue string) response.DashboardOver
return tx.Where("created_at >= ?", todayStart)
})
processingConversationCount := repositories.DashboardRepository.CountConversations(db, func(tx *gorm.DB) *gorm.DB {
return tx.Where("status = ?", enums.IMConversationStatusActive)
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)
@@ -45,6 +48,7 @@ func (s *dashboardService) GetOverview(rangeValue string) response.DashboardOver
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,
})
@@ -168,6 +172,8 @@ func (s *dashboardService) buildAgentStats(now time.Time, teams []models.AgentTe
teamCounters[item.CurrentTeamID] = counter
}
switch item.Status {
case enums.IMConversationStatusAIServing:
counter.processingConversations++
case enums.IMConversationStatusPending:
counter.waitingConversations++
case enums.IMConversationStatusActive:
@@ -229,7 +235,10 @@ func (s *dashboardService) buildAlerts(now time.Time, db *gorm.DB, activeConvers
}
staleProcessingCount := repositories.DashboardRepository.CountConversations(db, func(tx *gorm.DB) *gorm.DB {
return tx.Where("status = ? AND (last_message_at IS NULL OR last_message_at < ?)", enums.IMConversationStatusActive, activeTimeout)
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{
+3
View File
@@ -514,6 +514,9 @@ func (s *messageService) ValidateConversationSender(conversationID int64, sender
if operator == nil {
return nil, errorsx.Unauthorized("未登录或登录已过期")
}
if conversation.Status != enums.IMConversationStatusAIServing {
return nil, errorsx.Forbidden("当前会话不处于 AI 接待状态")
}
if conversation.CurrentAssigneeID != 0 {
return nil, errorsx.Forbidden("当前会话已由人工客服接管")
}
@@ -382,6 +382,7 @@ func (s *wxWorkKFInboundService) ensureConversation(base syncmsg.BaseMessage, pr
Eq("external_source", external.ExternalSource).
Eq("external_id", external.ExternalID).
In("status", []enums.IMConversationStatus{
enums.IMConversationStatusAIServing,
enums.IMConversationStatusPending,
enums.IMConversationStatusActive,
}).
@@ -49,13 +49,13 @@ type ConversationDetailDialogProps = {
function getStatusMeta(status: number) {
switch (status) {
case 1:
return { label: "待接入", variant: "outline" as const };
return { label: "AI接待中", variant: "secondary" as const };
case 2:
return { label: "处理中", variant: "secondary" as const };
return { label: "待接入", variant: "outline" as const };
case 3:
return { label: "已关闭", variant: "outline" as const };
return { label: "处理中", variant: "secondary" as const };
case 4:
return { label: "已归档", variant: "outline" as const };
return { label: "已关闭", variant: "outline" as const };
default:
return { label: "未知", variant: "outline" as const };
}
@@ -151,8 +151,8 @@ export function ConversationDetailDialog({
onOpenClose,
}: ConversationDetailDialogProps) {
const currentConversation = detail ?? item;
const isClosedConversation = currentConversation?.status === 3;
const isPendingConversation = currentConversation?.status === 1;
const isClosedConversation = currentConversation?.status === 4;
const isPendingConversation = currentConversation?.status === 2;
const statusMeta = currentConversation
? getStatusMeta(currentConversation.status)
: null;
@@ -274,7 +274,7 @@ export function ConversationDetailDialog({
<Button
variant="outline"
onClick={onOpenAssign}
disabled={saving || !currentConversation || isClosedConversation}
disabled={saving || !currentConversation || currentConversation.status !== 2}
>
<MessageCircleMoreIcon />
{saving ? "处理中..." : "分配会话"}
@@ -301,7 +301,7 @@ export function ConversationDetailDialog({
type="button"
variant="outline"
onClick={onOpenTransfer}
disabled={saving || !currentConversation || isClosedConversation}
disabled={saving || !currentConversation || currentConversation.status !== 3}
>
<MessageCircleMoreIcon />
{saving ? "处理中..." : "转接会话"}
+12 -12
View File
@@ -68,22 +68,22 @@ const RECONNECT_MAX_DELAY = 30000
const statusOptions = [
{ value: "all", label: "全部状态" },
{ value: "1", label: "待接入" },
{ value: "2", label: "处理中" },
{ value: "3", label: "已关闭" },
{ value: "4", label: "已归档" },
{ value: "1", label: "AI接待中" },
{ value: "2", label: "待接入" },
{ value: "3", label: "处理中" },
{ value: "4", label: "已关闭" },
] as const
function getStatusMeta(status: number) {
switch (status) {
case 1:
return { label: "待接入", variant: "outline" as const }
return { label: "AI接待中", variant: "secondary" as const }
case 2:
return { label: "处理中", variant: "secondary" as const }
return { label: "待接入", variant: "outline" as const }
case 3:
return { label: "已关闭", variant: "outline" as const }
return { label: "处理中", variant: "secondary" as const }
case 4:
return { label: "已归档", variant: "outline" as const }
return { label: "已关闭", variant: "outline" as const }
default:
return { label: "未知", variant: "outline" as const }
}
@@ -761,14 +761,14 @@ export default function DashboardConversationsPage() {
<DropdownMenuContent align="end" className="w-44 min-w-44">
<DropdownMenuItem
onClick={() => openAssign(item)}
disabled={actionLoadingId === item.id || item.status !== 1}
disabled={actionLoadingId === item.id || item.status !== 2}
>
<MessageCircleMoreIcon />
{actionLoadingId === item.id ? "处理中..." : "分配会话"}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => void handleDispatch(item)}
disabled={actionLoadingId === item.id || item.status !== 1}
disabled={actionLoadingId === item.id || item.status !== 2}
>
<RefreshCwIcon />
{actionLoadingId === item.id ? "处理中..." : "重试分配"}
@@ -782,12 +782,12 @@ export default function DashboardConversationsPage() {
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => openTransfer(item)}
disabled={actionLoadingId === item.id || item.status !== 2}
disabled={actionLoadingId === item.id || item.status !== 3}
>
<MessageCircleMoreIcon />
{actionLoadingId === item.id ? "处理中..." : "转接会话"}
</DropdownMenuItem>
{item.status !== 3 ? (
{item.status !== 4 ? (
<DropdownMenuItem
onClick={() => openClose(item)}
disabled={actionLoadingId === item.id}
@@ -86,8 +86,8 @@ export function ChatPanel() {
const [claimDialogOpen, setClaimDialogOpen] = useState(false);
const [transferDialogOpen, setTransferDialogOpen] = useState(false);
const isLgUp = useIsLgUp();
const isClosedConversation = conversation?.status === 3;
const isPendingConversation = conversation?.status === 1;
const isClosedConversation = conversation?.status === 4;
const isPendingConversation = conversation?.status === 2;
const showMessageEditor = !isClosedConversation && !isPendingConversation;
const currentUserId = readSession()?.user?.id ?? 0;
@@ -397,6 +397,10 @@ export function ChatPanel() {
<div className="bg-amber-500/10 px-4 py-3 text-sm text-amber-950 dark:bg-amber-500/15 dark:text-amber-100">
</div>
) : conversation?.status === 1 ? (
<div className="bg-violet-500/10 px-4 py-3 text-sm text-violet-950 dark:bg-violet-500/15 dark:text-violet-100">
AI
</div>
) : isPendingConversation ? (
<div className="bg-blue-500/10 px-4 py-3 dark:bg-blue-500/15">
<div className="flex items-center gap-2">
@@ -14,6 +14,8 @@ import { getEnumLabel } from "@/lib/enums"
function getStatusVariant(status: number) {
switch (status) {
case IMConversationStatus.AIServing:
return "bg-violet-500/15 text-violet-700 dark:bg-violet-500/20 dark:text-violet-300"
case IMConversationStatus.Pending:
return "bg-blue-500/15 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300"
case IMConversationStatus.Active:
+2 -2
View File
@@ -244,14 +244,14 @@ export default function ConversationsPage() {
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setTransferOpen(true)}
disabled={!conversation || conversation.status !== 2}
disabled={!conversation || conversation.status !== 3}
>
<ArrowRightLeftIcon />
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setCloseOpen(true)}
disabled={!conversation || conversation.status === 3}
disabled={!conversation || conversation.status === 4}
>
<CircleXIcon />
+5 -3
View File
@@ -112,11 +112,13 @@ export const IMConversationServiceModeLabels: Record<IMConversationServiceMode,
}
export enum IMConversationStatus {
Pending = 1,
Active = 2,
Closed = 3,
AIServing = 1,
Pending = 2,
Active = 3,
Closed = 4,
}
export const IMConversationStatusLabels: Record<IMConversationStatus, string> = {
[IMConversationStatus.AIServing]: "AI接待中",
[IMConversationStatus.Pending]: "待接入",
[IMConversationStatus.Active]: "处理中",
[IMConversationStatus.Closed]: "已关闭",
+1
View File
@@ -19,6 +19,7 @@ import { generateUUID } from "@/lib/utils"
export const agentConversationFilterOptions = [
// { value: "mine", label: "我的" },
{ value: "ai_serving", label: "AI接待中" },
{ value: "active", label: "处理中" },
{ value: "pending", label: "待接入" },
{ value: "closed", label: "已关闭" },