feat: add AI serving status to conversation management and update related components
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 ? "处理中..." : "转接会话"}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 />
|
||||
关闭会话
|
||||
|
||||
@@ -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]: "已关闭",
|
||||
|
||||
@@ -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: "已关闭" },
|
||||
|
||||
Reference in New Issue
Block a user