refactor: 将客服后端重构为宿主可嵌入模块

- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。

- 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。

- 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。

- 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
t
2026-08-28 22:23:13 +08:00
parent 6845c728f8
commit 18c9354095
377 changed files with 13199 additions and 22881 deletions
+30 -13
View File
@@ -23,7 +23,9 @@ func BuildConversationWithLocale(item *models.Conversation, locale string) respo
ID: item.ID,
AIAgentID: item.AIAgentID,
ChannelID: item.ChannelID,
CustomerType: item.CustomerType,
CustomerID: item.CustomerID,
CustomerExternalID: item.CustomerExternalID,
CustomerName: item.CustomerName,
Status: item.Status,
ServiceMode: item.ServiceMode,
@@ -47,6 +49,18 @@ func BuildConversationWithLocale(item *models.Conversation, locale string) respo
if identity := services.ConversationService.GetConversationExternalIdentity(item); identity != nil {
ret.CustomerOnline = services.WsService.IsGuestOnline(identity.ExternalID)
}
queueSnapshot := services.ConversationQueueService.GetSnapshot(item)
if queueSnapshot.Queued {
ret.QueueEnteredAt = utils.FormatTimePtr(queueSnapshot.EnteredAt)
ret.QueuePosition = queueSnapshot.Position
ret.QueueAheadCount = queueSnapshot.AheadCount
ret.QueueWaitingCount = queueSnapshot.WaitingCount
ret.QueueWaitSeconds = queueSnapshot.WaitSeconds
ret.QueueEstimatedWaitSeconds = queueSnapshot.EstimatedWaitSeconds
ret.QueueEscalationLevel = queueSnapshot.EscalationLevel
ret.EffectivePriority = queueSnapshot.EffectivePriority
ret.QueueServiceOnline = queueSnapshot.ServiceOnline
}
if item.CurrentAssigneeID > 0 {
if user := services.UserService.Get(item.CurrentAssigneeID); user != nil {
ret.CurrentAssigneeName = user.Nickname
@@ -120,11 +134,11 @@ func BuildMessagesWithLocale(list []models.Message, locale string) []response.Me
return nil
}
agentReadState, customerReadState := services.ConversationReadStateService.GetConversationReadStates(list[0].ConversationID)
aiSenderNames, userSenderNames := collectMessageSenderNameMaps(list)
aiSenders, userSenderNames := collectMessageSenderMaps(list)
agentProfiles := collectAgentProfilesByMessages(list)
ret := make([]response.MessageResponse, 0, len(list))
for i := range list {
ret = append(ret, BuildMessageWithReadStatesAndLocale(&list[i], agentReadState, customerReadState, aiSenderNames, userSenderNames, agentProfiles, locale))
ret = append(ret, BuildMessageWithReadStatesAndLocale(&list[i], agentReadState, customerReadState, aiSenders, userSenderNames, agentProfiles, locale))
}
return ret
}
@@ -138,17 +152,16 @@ func BuildMessageWithLocale(item *models.Message, locale string) response.Messag
return BuildMessageWithReadStatesAndLocale(item, agentReadState, customerReadState, nil, nil, nil, locale)
}
func BuildMessageWithReadStates(item *models.Message, agentReadState, customerReadState *models.ConversationReadState, aiSenderNames, userSenderNames map[int64]string, agentProfiles map[int64]*models.AgentProfile) response.MessageResponse {
return BuildMessageWithReadStatesAndLocale(item, agentReadState, customerReadState, aiSenderNames, userSenderNames, agentProfiles, i18nx.DefaultLocale)
func BuildMessageWithReadStates(item *models.Message, agentReadState, customerReadState *models.ConversationReadState, aiSenders map[int64]*models.AIAgent, userSenderNames map[int64]string, agentProfiles map[int64]*models.AgentProfile) response.MessageResponse {
return BuildMessageWithReadStatesAndLocale(item, agentReadState, customerReadState, aiSenders, userSenderNames, agentProfiles, i18nx.DefaultLocale)
}
func BuildMessageWithReadStatesAndLocale(item *models.Message, agentReadState, customerReadState *models.ConversationReadState, aiSenderNames, userSenderNames map[int64]string, agentProfiles map[int64]*models.AgentProfile, locale string) response.MessageResponse {
func BuildMessageWithReadStatesAndLocale(item *models.Message, agentReadState, customerReadState *models.ConversationReadState, aiSenders map[int64]*models.AIAgent, userSenderNames map[int64]string, agentProfiles map[int64]*models.AgentProfile, locale string) response.MessageResponse {
content, payload := utils.BuildRenderableMessage(item)
ret := response.MessageResponse{
ID: item.ID,
ConversationID: item.ConversationID,
RequestID: item.RequestID,
WorkflowRunID: item.WorkflowRunID,
ClientMsgID: item.ClientMsgID,
SenderType: item.SenderType,
SenderID: item.SenderID,
@@ -168,10 +181,13 @@ func BuildMessageWithReadStatesAndLocale(item *models.Message, agentReadState, c
}
if item.SenderID > 0 {
if item.SenderType == enums.IMSenderTypeAI {
if aiSenderNames != nil {
ret.SenderName = aiSenderNames[item.SenderID]
} else if aiAgent := services.AIAgentService.Get(item.SenderID); aiAgent != nil {
aiAgent := aiSenders[item.SenderID]
if aiAgent == nil {
aiAgent = services.AIAgentService.Get(item.SenderID)
}
if aiAgent != nil {
ret.SenderName = aiAgent.Name
ret.SenderAvatar = strings.TrimSpace(aiAgent.Avatar)
}
} else if item.SenderType == enums.IMSenderTypeAgent {
profile := agentProfiles[item.SenderID]
@@ -240,8 +256,8 @@ func collectAgentProfilesByMessages(list []models.Message) map[int64]*models.Age
return out
}
func collectMessageSenderNameMaps(list []models.Message) (aiNames map[int64]string, userNames map[int64]string) {
aiNames = make(map[int64]string)
func collectMessageSenderMaps(list []models.Message) (aiSenders map[int64]*models.AIAgent, userNames map[int64]string) {
aiSenders = make(map[int64]*models.AIAgent)
userNames = make(map[int64]string)
var aiIDs, userIDs []int64
seenAI := make(map[int64]struct{})
@@ -266,7 +282,8 @@ func collectMessageSenderNameMaps(list []models.Message) (aiNames map[int64]stri
userIDs = append(userIDs, m.SenderID)
}
for _, a := range services.AIAgentService.FindByIds(aiIDs) {
aiNames[a.ID] = a.Name
agent := a
aiSenders[a.ID] = &agent
}
for _, u := range services.UserService.FindByIds(userIDs) {
name := u.Nickname
@@ -275,7 +292,7 @@ func collectMessageSenderNameMaps(list []models.Message) (aiNames map[int64]stri
}
userNames[u.ID] = name
}
return aiNames, userNames
return aiSenders, userNames
}
func isMessageRead(item *models.Message, state *models.ConversationReadState) bool {