Files
customer-server/service/customer_service_account_service.go

856 lines
32 KiB
Go
Raw Normal View History

2026-03-14 10:02:49 +08:00
// Package service - 客服账号服务
// 功能:客服账号的增删改查业务逻辑
package service
import (
"context"
"customer-server/dao"
"customer-server/model/dto"
"customer-server/model/entity"
"customer-server/util"
"fmt"
"strings"
"time"
"gitea.com/red-future/common/beans"
commonMongo "gitea.com/red-future/common/db/mongo"
"gitea.com/red-future/common/ragflow"
"gitea.com/red-future/common/utils"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
var CustomerServiceAccount = new(customerServiceAccount)
type customerServiceAccount struct{}
// Add 添加客服账号
func (s *customerServiceAccount) Add(ctx context.Context, req *dto.AddCustomerServiceAccountReq) (res *dto.AddCustomerServiceAccountRes, err error) {
// 1. 检查客服ID是否已存在
coll := commonMongo.GetDB().Collection(entity.CustomerServiceAccountCollection)
filter := bson.M{"accountName": req.AccountName, "isDeleted": false}
count, err := coll.CountDocuments(ctx, filter)
if err != nil {
return nil, gerror.Wrap(err, "检查客服ID是否存在失败")
}
if count > 0 {
return nil, gerror.Newf("客服账号名称 '%s' 已存在,请使用其他名称", req.AccountName)
}
// 2. 准备数据但暂不写入MongoDB
data := &entity.CustomerServiceAccount{}
if err = utils.Struct(req, data); err != nil {
return
}
// 调试日志:输出请求和转换后的数据
g.Log().Infof(ctx, "[Add] 请求数据 - AccountName: %s, Platform: %s, Prompt: %v, Greeting: %v",
req.AccountName, req.Platform, req.Prompt, req.Greeting)
g.Log().Infof(ctx, "[Add] 转换后Entity - AccountName: %s, Platform: %s, Greeting: %s",
data.AccountName, data.Platform, data.Greeting)
now := gtime.Now().Time
data.CreatedAt = &now // 取地址赋值给指针类型
data.UpdatedAt = &now // 取地址赋值给指针类型
data.IsDeleted = false
data.IsDisabled = false
// 获取或设置tenantId统一转换为string存储
var tenantId string
if req.TenantId != nil {
tenantId = gconv.String(req.TenantId)
g.Log().Infof(ctx, "使用手动指定的tenant_id: %v", req.TenantId)
} else {
// 从session获取tenantId
tenantInfo, err := util.GetTenantInfo(ctx)
if err != nil {
return nil, gerror.Wrap(err, "无法获取租户信息")
}
tenantId = gconv.String(tenantInfo.TenantId)
if tenantId == "" {
return nil, gerror.New("无法获取租户ID")
}
}
// 统一使用string类型存储到MongoDB
data.TenantId = tenantId
// 3. 确保租户知识库存在(调用公共方法)
datasetId, err := EnsureTenantDataset(ctx, tenantId)
if err != nil {
g.Log().Errorf(ctx, "确保租户知识库存在失败: %v", err)
return nil, gerror.Wrap(err, "确保租户知识库存在失败")
}
// 4. 先插入客服账号必须在创建Chat之前因为createChatAndSaveConfig内部会通过accountName查询租户ID
err = dao.CustomerServiceAccount.Insert(ctx, data)
if err != nil {
return nil, gerror.Wrap(err, "插入客服账号失败")
}
// 5. 处理话术绑定
if len(req.SpeechcraftIds) > 0 {
g.Log().Infof(ctx, "开始同步话术到RAGFlow: count=%d", len(req.SpeechcraftIds))
for _, speechcraftId := range req.SpeechcraftIds {
_, err := Speechcraft.SyncToRAGFlow(ctx, speechcraftId, req.AccountName, tenantId)
if err != nil {
g.Log().Errorf(ctx, "同步话术到RAGFlow失败: speechcraft_id=%s, error=%v", speechcraftId, err)
}
}
}
// 6. 创建Chat并保存RAGFlow配置内部会轮询等待文档解析完成
var assistantDesc string
if req.SelfIdentity != nil {
assistantDesc = *req.SelfIdentity
}
if err = s.createChatAndSaveConfig(ctx, req.AccountName, req.Platform, tenantId, datasetId, assistantDesc); err != nil {
g.Log().Errorf(ctx, "创建Chat配置失败: %v", err)
return nil, gerror.Wrap(err, "创建Chat配置失败")
}
// 7. 如果提供了自定义提示词调用UpdatePrompt更新
if req.Prompt != nil && *req.Prompt != "" {
g.Log().Infof(ctx, "创建完成,开始更新自定义提示词")
updateReq := &dto.UpdatePromptReq{
AccountName: req.AccountName,
Prompt: *req.Prompt,
}
if _, err := RAGFlowConfig.UpdatePrompt(ctx, updateReq); err != nil {
g.Log().Errorf(ctx, "更新自定义提示词失败: %v", err)
// 不阻断创建流程,提示词可以后续修改
} else {
g.Log().Infof(ctx, "自定义提示词更新成功")
}
}
res = &dto.AddCustomerServiceAccountRes{Id: data.Id.Hex()}
return
}
// createChatAndSaveConfig 创建Chat并保存RAGFlow配置会轮询等待文档解析完成
func (s *customerServiceAccount) createChatAndSaveConfig(ctx context.Context, accountName, platform, tenantId, datasetId, assistantDescription string) error {
ragflowClient := ragflow.GetGlobalClient()
// 1. 轮询检查知识库中是否有解析完成的文档最多60秒
g.Log().Infof(ctx, "等待知识库文档解析完成: dataset_id=%s", datasetId)
maxWaitSeconds := 60
for i := 0; i < maxWaitSeconds; i++ {
listReq := &ragflow.ListDocumentsReq{
Page: 1,
PageSize: 1,
}
listRes, err := ragflowClient.ListDocuments(ctx, datasetId, listReq)
if err == nil && listRes != nil && len(listRes.Data.Docs) > 0 {
doc := listRes.Data.Docs[0]
// 调试:输出所有状态字段
if i == 0 || i%5 == 0 {
g.Log().Infof(ctx, "文档状态详情: dataset_id=%s, doc_id=%s, RunStatus=%s, Status=%s, ChunkCount=%d, Progress=%.2f, ProgressMsg=%s",
datasetId, doc.Id, doc.RunStatus, doc.Status, doc.ChunkCount, doc.Progress, doc.ProgressMsg)
}
// 检查解析失败状态Progress < 0 表示解析出错如embedding模型无法访问
if doc.Progress < 0 {
return gerror.Newf("RAGFlow文档解析失败embedding服务不可用: dataset_id=%s, doc_id=%s, error=%s",
datasetId, doc.Id, doc.ProgressMsg)
}
// 检查解析完成的条件ChunkCount > 0 表示已解析出分块
if doc.ChunkCount > 0 {
g.Log().Infof(ctx, "知识库文档解析完成: dataset_id=%s, doc_id=%s, chunk_count=%d, 耗时=%d秒",
datasetId, doc.Id, doc.ChunkCount, i)
break
}
}
// 超时检查
if i >= maxWaitSeconds-1 {
return gerror.Newf("等待文档解析超时: dataset_id=%s, 已等待%d秒", datasetId, maxWaitSeconds)
}
time.Sleep(time.Second)
}
// 2. 生成默认提示词、助理描述并创建Chat
promptText := s.generateDefaultPrompt()
modelName := s.getDefaultModelName(ctx)
// 使用传入的assistantDescription如果为空则使用默认生成
var assistantDesc string
if assistantDescription != "" {
assistantDesc = assistantDescription
} else {
assistantDesc = s.generateAssistantDescription(accountName, platform)
}
// Chat名称账号名_平台_租户ID
chatName := fmt.Sprintf("%s_%s_%s", accountName, platform, tenantId)
chatReq := &ragflow.CreateChatReq{
Name: chatName,
Description: assistantDesc,
DatasetIds: []string{datasetId},
Prompt: &ragflow.PromptConfig{
Prompt: promptText,
SimilarityThreshold: 0.2,
KeywordsSimilarityWeight: 0.7,
TopN: 8,
EmptyResponse: "",
Variables: []map[string]interface{}{
{
"key": "knowledge",
"optional": true,
},
},
},
Llm: &ragflow.Llm{
ModelName: modelName,
},
}
chat, err := ragflowClient.CreateChat(ctx, chatReq)
if err != nil {
// 如果Chat名称已存在尝试查询现有Chat
if strings.Contains(err.Error(), "Duplicated chat name") {
g.Log().Warningf(ctx, "Chat名称已存在尝试查询现有Chat: %s", chatName)
// 查询Chat列表查找同名的Chat
listReq := &ragflow.ListChatsReq{
Page: 1,
PageSize: 100,
}
listRes, listErr := ragflowClient.ListChats(ctx, listReq)
if listErr != nil {
return gerror.Wrapf(listErr, "查询Chat列表失败")
}
// 检查listRes和Data是否为nil防止遍历nil切片导致空指针异常
if listRes == nil || listRes.Data == nil {
return gerror.New("查询Chat列表返回空对象")
}
// 查找同名Chat
for _, existingChat := range listRes.Data {
if existingChat.Name == chatName {
g.Log().Infof(ctx, "找到现有Chat: name=%s, id=%s复用该Chat", chatName, existingChat.Id)
chat = existingChat
break
}
}
if chat == nil {
return gerror.Newf("找不到同名Chat: %s", chatName)
}
} else {
return gerror.Wrapf(err, "创建RAGFlow Chat失败")
}
}
// 最终检查chat是否为nil防止CreateChat返回(nil, nil)或Duplicated情况下未找到同名Chat
if chat == nil {
return gerror.New("创建RAGFlow Chat返回空对象")
}
// 先查询现有配置,保留用户的自定义设置
coll := commonMongo.GetDB().Collection(entity.RAGFlowConfigCollection)
filter := bson.M{"accountName": accountName, "isDeleted": false}
var existingConfig entity.RAGFlowConfig
findErr := coll.FindOne(ctx, filter).Decode(&existingConfig)
now := gtime.Now().Time
// 如果已有配置保留用户自定义的prompt和参数
if findErr == nil && !existingConfig.Id.IsZero() {
g.Log().Infof(ctx, "保留现有配置的自定义设置仅更新chat_id和dataset_id")
// 只更新必要字段,保留用户自定义内容
update := bson.M{
"$set": bson.M{
"chatId": chat.Id,
"datasetId": datasetId,
"datasetIds": []string{datasetId},
"syncStatus": "synced",
"updatedAt": now,
},
}
if _, err := coll.UpdateOne(ctx, filter, update); err != nil {
return gerror.Wrap(err, "更新RAGFlowConfig失败")
}
} else {
// 首次创建配置,使用默认值
g.Log().Infof(ctx, "首次创建RAGFlow配置使用默认提示词")
config := &entity.RAGFlowConfig{
AccountName: accountName,
Platform: platform,
DatasetId: datasetId,
DatasetIds: []string{datasetId},
DatasetName: "租户话术知识库",
ChatId: chat.Id,
Prompt: promptText,
DocumentIds: []string{},
SimilarityThreshold: 0.2,
KeywordsSimilarityWeight: 0.7,
TopN: 8,
EmptyResponse: "",
SyncStatus: "synced",
}
// 统一使用string类型存储tenantId到MongoDB
config.TenantId = tenantId
config.CreatedAt = &now // 取地址赋值给指针类型
config.UpdatedAt = &now // 取地址赋值给指针类型
config.IsDeleted = false
update := bson.M{"$set": config}
opts := options.UpdateOne().SetUpsert(true)
if _, err := coll.UpdateOne(ctx, filter, update, opts); err != nil {
return gerror.Wrap(err, "更新RAGFlowConfig失败")
}
}
g.Log().Infof(ctx, "RAGFlowConfig创建/更新成功: account_name=%s, chat_id=%s, dataset_id=%s",
accountName, chat.Id, datasetId)
return nil
}
// generateDefaultPrompt 生成默认提示词(包含知识库引用)
func (s *customerServiceAccount) generateDefaultPrompt() string {
// 默认提示词已包含完整的知识库引用格式
return `你是一个智能助手请总结知识库的内容来回答问题请列举知识库中的数据详细回答当所有知识库内容都与问题无关时你的回答必须包括"知识库中未找到您要的答案!"这句话回答需要考虑聊天历史
以下是知识库
{knowledge}
以上是知识库`
}
// generateAssistantDescription 根据客服账号生成助理描述
func (s *customerServiceAccount) generateAssistantDescription(accountName, platform string) string {
// 根据账号名称判断业务方向
lowerName := gstr.ToLower(accountName)
// 气血方向
if gstr.Contains(lowerName, "qixue") || gstr.Contains(lowerName, "气血") {
return "专业的女性健康顾问,专注于月经调理、气血养护等健康问题。以温暖关怀的态度为客户提供专业建议,帮助改善身体状况。"
}
// 减肥方向
if gstr.Contains(lowerName, "jianfei") || gstr.Contains(lowerName, "减肥") || gstr.Contains(lowerName, "shoushen") || gstr.Contains(lowerName, "瘦身") {
return "专业的减肥瘦身顾问,提供科学健康的减肥方案和营养建议。以积极正面的态度鼓励客户坚持健康的生活方式,达成理想体型。"
}
// 护肤方向
if gstr.Contains(lowerName, "hufu") || gstr.Contains(lowerName, "护肤") || gstr.Contains(lowerName, "meirong") || gstr.Contains(lowerName, "美容") {
return "专业的护肤美容顾问,为客户提供个性化的护肤方案和产品建议。帮助客户解决各类肌肤问题,重现健康光彩。"
}
// 养生方向
if gstr.Contains(lowerName, "yangsheng") || gstr.Contains(lowerName, "养生") || gstr.Contains(lowerName, "health") {
return "专业的养生健康顾问,提供中医养生、日常保健等全方位健康建议。倡导健康生活方式,帮助客户提升整体身心健康。"
}
// 默认通用描述
return fmt.Sprintf("智能客服助理,为您提供专业的咨询服务。基于%s平台随时解答您的疑问提供贴心的服务体验。", platform)
}
// getDefaultModelName 获取默认模型名称Consul优先config.yml兜底
func (s *customerServiceAccount) getDefaultModelName(ctx context.Context) string {
// 使用统一配置读取方法自动支持Consul动态配置
model := GetConfigString(ctx, "ragflow.default_model")
if model != "" {
return model
}
// 硬编码兜底(理论上不会走到这里)
return "qwen3-235b-a22b-instruct-2507"
}
// Update 更新客服账号
func (s *customerServiceAccount) Update(ctx context.Context, req *dto.UpdateCustomerServiceAccountReq) (err error) {
// 调试日志:输出请求数据
g.Log().Infof(ctx, "[Update] 请求数据 - Id: %s, AccountName: %s, Platform: %s, Prompt: %v, Greeting: %v",
req.Id, req.AccountName, req.Platform, req.Prompt, req.Greeting)
// 1. 检查账号名称是否重复
if req.AccountName != "" {
existingAccount, err := dao.CustomerServiceAccount.FindByAccountName(ctx, req.AccountName)
if err != nil && err != mongo.ErrNoDocuments {
return err
}
if existingAccount != nil && existingAccount.Id.Hex() != req.Id {
return gerror.Newf("客服账号名称 '%s' 已被其他账号使用,请使用其他名称", req.AccountName)
}
}
// 2. 更新基本信息accountName, platform
if err = dao.CustomerServiceAccount.Update(ctx, req); err != nil {
g.Log().Errorf(ctx, "[Update] 更新基本信息失败: %v", err)
return err
}
// 3. 查询客服账号获取accountName用于后续更新
objectId, err := bson.ObjectIDFromHex(req.Id)
if err != nil {
return gerror.Wrap(err, "无效的账号ID")
}
var account entity.CustomerServiceAccount
filter := bson.M{"_id": objectId, "isDeleted": false}
if err = dao.MongoDAO.FindOne(ctx, filter, &account, entity.CustomerServiceAccountCollection); err != nil {
return gerror.Wrap(err, "查询客服账号失败")
}
// 4. 如果提供了开场白更新greeting字段到customer_service_account表
if req.Greeting != nil {
g.Log().Infof(ctx, "[Update] 更新开场白 - accountName: %s, greeting: %s", account.AccountName, *req.Greeting)
updateReq := &dto.UpdateGreetingReq{
AccountName: account.AccountName,
Greeting: *req.Greeting,
}
if err = s.UpdateGreeting(ctx, updateReq); err != nil {
g.Log().Errorf(ctx, "[Update] 更新开场白失败: %v", err)
return gerror.Wrap(err, "更新开场白失败")
}
}
// 5. 如果提供了提示词更新RAGFlow配置
if req.Prompt != nil {
g.Log().Infof(ctx, "[Update] 更新提示词 - accountName: %s", account.AccountName)
updatePromptReq := &dto.UpdatePromptReq{
AccountName: account.AccountName,
Prompt: *req.Prompt,
}
if _, err = RAGFlowConfig.UpdatePrompt(ctx, updatePromptReq); err != nil {
g.Log().Errorf(ctx, "[Update] 更新提示词失败: %v", err)
return gerror.Wrap(err, "更新提示词失败")
}
}
// 6. 如果accountName、platform或selfIdentity有变化同步更新RAGFlow Chat的name和description
if req.AccountName != "" || req.Platform != "" || req.SelfIdentity != nil {
if err = s.updateRAGFlowChatInfo(ctx, &account, req); err != nil {
g.Log().Errorf(ctx, "[Update] 同步更新RAGFlow Chat失败: %v", err)
return gerror.Wrap(err, "同步更新RAGFlow Chat失败")
}
}
g.Log().Infof(ctx, "[Update] 客服账号更新成功 - accountName: %s", account.AccountName)
return nil
}
// UpdateGreeting 更新开场白
func (s *customerServiceAccount) UpdateGreeting(ctx context.Context, req *dto.UpdateGreetingReq) (err error) {
// 获取租户ID租户隔离
user, err := util.GetTenantInfo(ctx)
if err != nil {
return gerror.Wrap(err, "获取租户信息失败")
}
filter := bson.M{
"accountName": req.AccountName,
"tenantId": user.TenantId,
"isDeleted": false,
}
update := bson.M{
"$set": bson.M{
"greeting": req.Greeting,
"updatedAt": gtime.Now().Time,
"updater": user.UserName,
},
}
g.Log().Infof(ctx, "[UpdateGreeting] 开始更新开场白 - accountName: %s, tenantId: %v, greeting: %s",
req.AccountName, user.TenantId, req.Greeting)
g.Log().Infof(ctx, "[UpdateGreeting] 查询条件 - filter: %+v", filter)
g.Log().Infof(ctx, "[UpdateGreeting] 更新内容 - update: %+v", update)
// 使用MongoDAO.UpdateOne不需要token验证避免双重验证冲突
matchedCount, modifiedCount, err := dao.MongoDAO.UpdateOne(ctx, filter, update, entity.CustomerServiceAccountCollection)
if err != nil {
g.Log().Errorf(ctx, "[UpdateGreeting] 数据库更新失败 - err: %v", err)
return gerror.Wrapf(err, "更新开场白失败")
}
g.Log().Infof(ctx, "[UpdateGreeting] 数据库更新结果 - matchedCount: %d, modifiedCount: %d", matchedCount, modifiedCount)
if modifiedCount == 0 {
if matchedCount == 0 {
g.Log().Errorf(ctx, "[UpdateGreeting] 未找到匹配记录 - accountName: %s, tenantId: %v", req.AccountName, user.TenantId)
return gerror.Newf("客服账号不存在或不属于当前租户: accountName=%s", req.AccountName)
}
g.Log().Warningf(ctx, "[UpdateGreeting] 记录已存在但未修改(内容相同) - accountName: %s", req.AccountName)
}
g.Log().Infof(ctx, "[UpdateGreeting] ✅ 开场白更新成功 - accountName: %s, 更新记录数: %d", req.AccountName, modifiedCount)
return
}
// ToggleStatus 切换客服账号状态(启用/禁用)
// 参数: ctx - 上下文req - 状态切换请求包含账号ID
// 返回: err - 错误信息
// 功能: 在启用和禁用状态之间切换,禁用后不再接收用户消息
func (s *customerServiceAccount) ToggleStatus(ctx context.Context, req *dto.ToggleCustomerServiceAccountStatusReq) (err error) {
return dao.CustomerServiceAccount.ToggleStatus(ctx, req)
}
// List 获取客服账号列表关联查询prompt字段
// 参数: ctx - 上下文req - 列表查询请求(支持分页、平台筛选、状态筛选)
// 返回: res - 客服账号列表及分页信息包含prompt字段err - 错误信息
// 功能: 分页查询客服账号并从ragflow_config表关联查询每个账号的prompt去除知识库引用部分
func (s *customerServiceAccount) List(ctx context.Context, req *dto.ListCustomerServiceAccountReq) (res *dto.ListCustomerServiceAccountRes, err error) {
list, total, err := dao.CustomerServiceAccount.List(ctx, req)
if err != nil {
return
}
// 关联查询每个账号的prompt从ragflow_config表
for i := range list {
account := list[i]
if config, configErr := dao.RAGFlowConfig.FindByAccountName(ctx, account.AccountName); configErr == nil && config != nil {
// 去除知识库引用部分,只返回用户输入的业务提示词
userPrompt := config.Prompt
// 查找知识库引用的起始位置(支持多种格式)
knowledgePatterns := []string{
"\n\n以下是知识库",
"\n\n以下是知识库",
"\n以下是知识库",
"\n以下是知识库",
}
// 找到最早出现的知识库引用位置并截断
for _, pattern := range knowledgePatterns {
if idx := gstr.Pos(userPrompt, pattern); idx >= 0 {
userPrompt = gstr.SubStr(userPrompt, 0, idx)
break
}
}
// 去除末尾空白
userPrompt = gstr.TrimRight(userPrompt)
// 将处理后的prompt赋值给account
list[i].Prompt = userPrompt
g.Log().Debugf(ctx, "账号 %s 的prompt已关联原始长度: %d, 处理后长度: %d",
account.AccountName, len(config.Prompt), len(userPrompt))
}
}
res = &dto.ListCustomerServiceAccountRes{
List: list,
Total: int(total),
}
return
}
// Delete 删除客服账号(软删除 + 删除RAGFlow Chat
// 参数: ctx - 上下文req - 删除客服账号请求包含账号ID
// 返回: res - 删除结果信息err - 错误信息
// 功能: 逻辑删除客服账号同时删除RAGFlow中的Chat配置和ragflow_config记录
func (s *customerServiceAccount) Delete(ctx context.Context, req *dto.DeleteCustomerServiceAccountReq) (res *dto.DeleteCustomerServiceAccountRes, err error) {
res = &dto.DeleteCustomerServiceAccountRes{}
// 1. 查询客服账号信息使用MongoDAO直接查询避免租户过滤
objectId, err := bson.ObjectIDFromHex(req.Id)
if err != nil {
return nil, gerror.Wrap(err, "无效的账号ID")
}
var account entity.CustomerServiceAccount
filter := bson.M{"_id": objectId, "isDeleted": false}
err = dao.MongoDAO.FindOne(ctx, filter, &account, entity.CustomerServiceAccountCollection)
if err != nil {
if err.Error() == "mongo: no documents in result" {
return nil, gerror.New("客服账号不存在")
}
return nil, gerror.Wrap(err, "查询客服账号失败")
}
// 2. 查询关联的RAGFlowConfig获取chatId
config, err := dao.RAGFlowConfig.FindByAccountName(ctx, account.AccountName)
if err != nil {
g.Log().Warningf(ctx, "查询RAGFlowConfig失败: %v", err)
}
// 3. 如果存在chatId调用RAGFlow API删除Chat
if config != nil && config.ChatId != "" {
ragflowClient := ragflow.GetGlobalClient()
if err := ragflowClient.DeleteChats(ctx, []string{config.ChatId}); err != nil {
g.Log().Errorf(ctx, "删除RAGFlow Chat失败: chat_id=%s, error=%v", config.ChatId, err)
// 不阻断删除流程,记录错误日志
} else {
g.Log().Infof(ctx, "RAGFlow Chat删除成功: chat_id=%s", config.ChatId)
}
// 4. 软删除RAGFlowConfig记录
filter := bson.M{"_id": config.Id, "isDeleted": false}
update := bson.M{"$set": bson.M{"isDeleted": true}}
if _, err := commonMongo.DB().Update(ctx, filter, update, entity.RAGFlowConfigCollection); err != nil {
g.Log().Warningf(ctx, "软删除RAGFlowConfig失败: %v", err)
}
}
// 5. 软删除客服账号
if err := dao.CustomerServiceAccount.Delete(ctx, account.Id.Hex()); err != nil {
return nil, gerror.Wrap(err, "删除客服账号失败")
}
g.Log().Infof(ctx, "客服账号删除成功: account_name=%s, chat_id=%s",
account.AccountName, config.ChatId)
res.Success = true
res.Message = "删除成功"
return
}
// getAccessibleSpeechcraftsInternal 内部方法:获取客服账号可访问的话术(动态权限)
// 参数: ctx - 上下文accountName - 客服账号名
// 返回: speechcrafts - 话术列表err - 错误信息
// 功能: 根据客服账号的speechcraft_ids字段判断权限范围
//
// 空数组 = 返回该租户下所有话术(动态权限)
// 非空 = 只返回指定ID的话术指定权限
func (s *customerServiceAccount) getAccessibleSpeechcraftsInternal(ctx context.Context, accountName string) (speechcrafts []*entity.Speechcraft, err error) {
// 1. 查询客服账号
account, err := dao.CustomerServiceAccount.FindByAccountName(ctx, accountName)
if err != nil {
return nil, gerror.Wrapf(err, "查询客服账号失败")
}
if account == nil {
return nil, gerror.New("客服账号不存在")
}
// 2. 根据speechcraft_ids判断权限范围
if len(account.SpeechcraftIds) == 0 {
// 空数组 = 动态权限,返回该租户下所有话术
filter := bson.M{"isDeleted": false}
page := &beans.Page{PageNum: 1, PageSize: 10000} // 查询所有话术
orderBy := []beans.OrderBy{}
_, err = commonMongo.DB().Find(ctx, filter, &speechcrafts, entity.SpeechcraftCollection, page, orderBy)
if err != nil {
return nil, gerror.Wrapf(err, "查询所有话术失败")
}
g.Log().Infof(ctx, "客服账号使用动态权限account_name=%s, speechcraft_count=%d", accountName, len(speechcrafts))
} else {
// 非空 = 指定权限只返回指定ID的话术
var objectIds []bson.ObjectID
for _, idStr := range account.SpeechcraftIds {
if oid, err := bson.ObjectIDFromHex(idStr); err == nil {
objectIds = append(objectIds, oid)
}
}
if len(objectIds) > 0 {
filter := bson.M{
"_id": bson.M{"$in": objectIds},
"isDeleted": false,
}
page := &beans.Page{PageNum: 1, PageSize: 10000} // 查询指定话术
orderBy := []beans.OrderBy{}
_, err = commonMongo.DB().Find(ctx, filter, &speechcrafts, entity.SpeechcraftCollection, page, orderBy)
if err != nil {
return nil, gerror.Wrapf(err, "查询指定话术失败")
}
}
g.Log().Infof(ctx, "客服账号使用指定权限account_name=%s, allowed_count=%d, found_count=%d",
accountName, len(account.SpeechcraftIds), len(speechcrafts))
}
return speechcrafts, nil
}
// GetAccessibleSpeechcrafts 获取客服账号可访问的话术列表API接口
// 参数: ctx - 上下文req - 查询请求(包含账号名)
// 返回: res - 话术列表及权限类型dynamic/specifiederr - 错误信息
// 功能: 查询客服账号可访问的话术返回格式化的DTO列表和权限标识
func (s *customerServiceAccount) GetAccessibleSpeechcrafts(ctx context.Context, req *dto.GetAccessibleSpeechcraftsReq) (res *dto.GetAccessibleSpeechcraftsRes, err error) {
res = &dto.GetAccessibleSpeechcraftsRes{}
// 1. 查询客服账号
account, err := dao.CustomerServiceAccount.FindByAccountName(ctx, req.AccountName)
if err != nil {
return nil, gerror.Wrapf(err, "查询客服账号失败")
}
if account == nil {
return nil, gerror.New("客服账号不存在")
}
// 2. 获取话术列表
speechcrafts, err := s.getAccessibleSpeechcraftsInternal(ctx, req.AccountName)
if err != nil {
return nil, err
}
// 3. 转换为DTO格式
for _, sc := range speechcrafts {
item := dto.SpeechcraftItem{
Id: sc.Id.Hex(),
Tag: sc.Tag,
Content: sc.Content,
Direction: sc.Direction,
Platform: sc.Platform,
Keywords: sc.Keywords,
}
res.Speechcrafts = append(res.Speechcrafts, item)
}
res.TotalCount = len(res.Speechcrafts)
// 4. 标记权限类型
if len(account.SpeechcraftIds) == 0 {
res.Permission = "dynamic" // 动态权限:所有话术
} else {
res.Permission = "specified" // 指定权限:限定话术
}
return res, nil
}
// RecreateRAGFlowConfig 重新创建RAGFlow配置
// 参数: ctx - 上下文req - 重建请求(包含账号名和平台)
// 返回: err - 错误信息
// 功能: 删除旧配置并重新创建Dataset和Chat用于修复创建客服账号时RAGFlow配置创建失败的情况或chat被删除后自动重建
func (s *customerServiceAccount) RecreateRAGFlowConfig(ctx context.Context, req *dto.RecreateRAGFlowConfigReq) (err error) {
// 1. 查询客服账号
account, err := dao.CustomerServiceAccount.FindByAccountName(ctx, req.AccountName)
if err != nil {
return gerror.Wrap(err, "查询客服账号失败")
}
if account == nil {
return gerror.Newf("客服账号 %s 不存在", req.AccountName)
}
// 2. 获取租户ID
tenantId := gconv.String(account.TenantId)
if tenantId == "" {
return gerror.New("客服账号的租户ID为空")
}
accountName := req.AccountName
// 3. 查询现有RAGFlow配置带租户隔离
coll := commonMongo.GetDB().Collection(entity.RAGFlowConfigCollection)
filter := bson.M{"accountName": accountName, "tenantId": account.TenantId, "isDeleted": false}
var existingConfig entity.RAGFlowConfig
err = coll.FindOne(ctx, filter).Decode(&existingConfig)
// 4. 如果已有配置且chat_id不为空验证chat是否真实存在于RAGFlow
needRecreate := false
if err == nil && !existingConfig.Id.IsZero() && existingConfig.ChatId != "" {
// 调用RAGFlow API验证chat是否存在通过UpdateChat验证
ragflowClient := ragflow.GetGlobalClient()
if ragflowClient != nil {
// 尝试更新chat即使不改任何参数如果chat不存在会返回错误
updateReq := &ragflow.UpdateChatReq{
DatasetIds: existingConfig.DatasetIds,
}
updateErr := ragflowClient.UpdateChat(ctx, existingConfig.ChatId, updateReq)
if updateErr != nil {
// chat不存在或更新失败需要重建
g.Log().Warningf(ctx, "RAGFlow中的Chat已被删除或不可用需要重建 - accountName: %s, old_chat_id: %s, error: %v",
accountName, existingConfig.ChatId, updateErr)
needRecreate = true
} else {
// chat存在且有效无需重建
g.Log().Infof(ctx, "发现现有有效的RAGFlow配置无需重建 - accountName: %s, chat_id: %s", accountName, existingConfig.ChatId)
return nil
}
} else {
// ragflowClient为空无法验证默认需要重建
needRecreate = true
}
} else {
// 配置不存在或chat_id为空需要创建
needRecreate = true
}
// 5. 需要重建chat
if !needRecreate {
return nil
}
g.Log().Infof(ctx, "开始创建新Chat - accountName: %s", accountName)
// 6. 确保租户知识库存在(调用公共方法)
datasetId, err := EnsureTenantDataset(ctx, tenantId)
if err != nil {
g.Log().Errorf(ctx, "确保租户知识库存在失败: %v", err)
return gerror.Wrap(err, "确保租户知识库存在失败")
}
// 7. 创建Chat并保存RAGFlow配置重建时使用账号的selfIdentity
if err = s.createChatAndSaveConfig(ctx, req.AccountName, req.Platform, tenantId, datasetId, account.SelfIdentity); err != nil {
g.Log().Errorf(ctx, "创建Chat配置失败: %v", err)
return gerror.Wrap(err, "创建Chat配置失败")
}
g.Log().Infof(ctx, "成功为客服账号 %s 创建RAGFlow配置", req.AccountName)
return nil
}
// updateRAGFlowChatInfo 同步更新RAGFlow Chat的name和description
func (s *customerServiceAccount) updateRAGFlowChatInfo(ctx context.Context, account *entity.CustomerServiceAccount, req *dto.UpdateCustomerServiceAccountReq) (err error) {
// 1. 查询RAGFlow配置获取chatId
config, err := dao.RAGFlowConfig.FindByAccountName(ctx, account.AccountName)
if err != nil {
g.Log().Warningf(ctx, "查询RAGFlow配置失败跳过同步: %v", err)
return nil
}
if config == nil || config.ChatId == "" {
g.Log().Warningf(ctx, "RAGFlow配置不存在或chatId为空跳过同步")
return nil
}
// 2. 构建新的chat名称和描述
tenantId := gconv.String(account.TenantId)
// 使用更新后的accountName和platform如果没有提供则使用原值
accountName := account.AccountName
platform := account.Platform
if req.AccountName != "" {
accountName = req.AccountName
}
if req.Platform != "" {
platform = req.Platform
}
newChatName := fmt.Sprintf("%s_%s_%s", accountName, platform, tenantId)
// 使用更新后的selfIdentity如果没有提供则使用原值
var newDescription string
if req.SelfIdentity != nil {
newDescription = *req.SelfIdentity
} else {
newDescription = account.SelfIdentity
}
// 如果selfIdentity为空使用默认生成方式
if newDescription == "" {
newDescription = s.generateAssistantDescription(accountName, platform)
}
g.Log().Infof(ctx, "[updateRAGFlowChatInfo] 同步更新RAGFlow Chat - chatId: %s, newName: %s, newDescription: %s",
config.ChatId, newChatName, newDescription)
// 3. 调用RAGFlow API更新Chat
ragflowClient := ragflow.GetGlobalClient()
if ragflowClient == nil {
return gerror.New("RAGFlow客户端未初始化")
}
updateReq := &ragflow.UpdateChatReq{
Name: newChatName,
Description: newDescription,
}
if err = ragflowClient.UpdateChat(ctx, config.ChatId, updateReq); err != nil {
g.Log().Errorf(ctx, "[updateRAGFlowChatInfo] 更新RAGFlow Chat失败: %v", err)
return gerror.Wrap(err, "更新RAGFlow Chat失败")
}
g.Log().Infof(ctx, "[updateRAGFlowChatInfo] ✅ 成功同步更新RAGFlow Chat")
return nil
}