// 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/specified),err - 错误信息 // 功能: 查询客服账号可访问的话术,返回格式化的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 }