Bladeren bron

Merge branch 'feature/deepseek_rag_1.0' of eta_server/eta_api into master

chenhan 3 weken geleden
bovenliggende
commit
08574bfdd8
58 gewijzigde bestanden met toevoegingen van 7799 en 5 verwijderingen
  1. 53 0
      cache/wechat_platform.go
  2. 420 0
      controllers/llm/abstract.go
  3. 186 0
      controllers/llm/chat_ws_controller.go
  4. 51 0
      controllers/llm/kb_controller.go
  5. 20 0
      controllers/llm/llm_http/request.go
  6. 17 0
      controllers/llm/llm_http/response.go
  7. 233 0
      controllers/llm/question.go
  8. 332 0
      controllers/llm/user_chat_controller.go
  9. 777 0
      controllers/llm/wechat_platform.go
  10. 5 1
      controllers/sys_role.go
  11. 1 0
      controllers/user_login.go
  12. 0 2
      main.go
  13. 31 0
      models/business_conf.go
  14. 40 0
      models/llm/user_chat_record.go
  15. 73 0
      models/llm/user_llm_chat.go
  16. 135 0
      models/rag/question.go
  17. 33 0
      models/rag/request/wechat_platform.go
  18. 11 0
      models/rag/response/abstract.go
  19. 11 0
      models/rag/response/question.go
  20. 30 0
      models/rag/response/wechat_platform.go
  21. 83 0
      models/rag/tag.go
  22. 302 0
      models/rag/wechat_article.go
  23. 255 0
      models/rag/wechat_article_abstract.go
  24. 66 0
      models/rag/wechat_article_chat_record.go
  25. 184 0
      models/rag/wechat_platform.go
  26. 82 0
      models/rag/wechat_platform_tag_mapping.go
  27. 88 0
      models/rag/wechat_platform_user_mapping.go
  28. 1 0
      models/system/sys_user.go
  29. 225 0
      routers/commentsRouter.go
  30. 11 0
      routers/router.go
  31. 302 0
      services/elastic/wechat_article.go
  32. 317 0
      services/elastic/wechat_article_abstract.go
  33. 212 0
      services/llm/base_wechat_lib.go
  34. 304 0
      services/llm/chat.go
  35. 213 0
      services/llm/chat_service.go
  36. 11 0
      services/llm/facade/bus_response/bus_response.go
  37. 8 0
      services/llm/facade/bus_response/eta_response.go
  38. 44 0
      services/llm/facade/llm_service.go
  39. 76 0
      services/task.go
  40. 1030 0
      services/wechat_platform.go
  41. 134 0
      services/ws_service.go
  42. 100 0
      utils/common.go
  43. 17 0
      utils/config.go
  44. 3 1
      utils/constants.go
  45. 252 0
      utils/llm/eta_llm/eta_llm_client.go
  46. 44 0
      utils/llm/eta_llm/eta_llm_http/request.go
  47. 63 0
      utils/llm/eta_llm/eta_llm_http/response.go
  48. 27 0
      utils/llm/llm_client.go
  49. 38 0
      utils/llm/llm_factory.go
  50. 106 0
      utils/lock/distrubtLock.go
  51. 6 1
      utils/redis.go
  52. 37 0
      utils/redis/cluster_redis.go
  53. 34 0
      utils/redis/standalone_redis.go
  54. 72 0
      utils/sql.go
  55. 93 0
      utils/ws/latency_measurer.go
  56. 113 0
      utils/ws/limiter.go
  57. 163 0
      utils/ws/session.go
  58. 224 0
      utils/ws/session_manager.go

+ 53 - 0
cache/wechat_platform.go

@@ -0,0 +1,53 @@
+package cache
+
+import (
+	"eta/eta_api/utils"
+	"fmt"
+)
+
+type WechatArticleOp struct {
+	Source           string
+	WechatPlatformId int
+}
+
+// AddWechatArticleOpToCache
+// @Description: 将公众号文章操作加入缓存
+// @param wechatPlatformId
+// @param source
+// @return bool
+func AddWechatArticleOpToCache(wechatPlatformId int, source string) bool {
+	record := new(WechatArticleOp)
+	record.Source = source
+	record.WechatPlatformId = wechatPlatformId
+	if utils.Re == nil {
+		err := utils.Rc.LPush(utils.CACHE_WECHAT_PLATFORM_ARTICLE, record)
+
+		utils.FileLog.Info(fmt.Sprintf("将公众号文章操作 加入缓存 AddWechatArticleOpToCache LPush: 操作类型:%s,公众号id:%d", source, wechatPlatformId))
+		if err != nil {
+			fmt.Println("AddWechatArticleOpToCache LPush Err:" + err.Error())
+		}
+		return true
+	}
+	return false
+}
+
+// AddWechatArticleLlmOpToCache
+// @Description: 将公众号文章llm操作加入缓存
+// @param wechatPlatformId
+// @param source
+// @return bool
+func AddWechatArticleLlmOpToCache(wechatPlatformId int, source string) bool {
+	record := new(WechatArticleOp)
+	record.Source = source
+	record.WechatPlatformId = wechatPlatformId
+	if utils.Re == nil {
+		err := utils.Rc.LPush(utils.CACHE_WECHAT_PLATFORM_ARTICLE_KNOWLEDGE, record)
+
+		utils.FileLog.Info(fmt.Sprintf("将公众号文章llm操作加入缓存 加入缓存 AddWechatArticleLlmOpToCache LPush: 操作类型:%s,公众号id:%d", source, wechatPlatformId))
+		if err != nil {
+			fmt.Println("AddWechatArticleOpToCache LPush Err:" + err.Error())
+		}
+		return true
+	}
+	return false
+}

+ 420 - 0
controllers/llm/abstract.go

@@ -0,0 +1,420 @@
+package llm
+
+import (
+	"encoding/json"
+	"eta/eta_api/cache"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/models/rag"
+	"eta/eta_api/models/rag/request"
+	"eta/eta_api/models/rag/response"
+	"eta/eta_api/services"
+	"eta/eta_api/services/elastic"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/paging"
+)
+
+// AbstractController
+// @Description: 摘要管理
+type AbstractController struct {
+	controllers.BaseAuthController
+}
+
+// List
+// @Title 列表
+// @Description 列表
+// @Param   PageSize   query   int  true       "每页数据条数"
+// @Param   CurrentIndex   query   int  true       "当前页页码,从1开始"
+// @Param   KeyWord   query   string  true       "搜索关键词"
+// @Success 200 {object} []*rag.QuestionListListResp
+// @router /abstract/list [get]
+func (c *AbstractController) List() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+
+	sysUser := c.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		return
+	}
+	pageSize, _ := c.GetInt("PageSize")
+	currentIndex, _ := c.GetInt("CurrentIndex")
+	keyWord := c.GetString("KeyWord")
+	tagId, _ := c.GetInt("TagId")
+
+	var startSize int
+	if pageSize <= 0 {
+		pageSize = utils.PageSize20
+	}
+	if currentIndex <= 0 {
+		currentIndex = 1
+	}
+	startSize = utils.StartIndex(currentIndex, pageSize)
+
+	// 获取列表
+	total, viewList, err := getAbstractList(keyWord, tagId, startSize, pageSize)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取失败,Err:" + err.Error()
+		return
+	}
+
+	page := paging.GetPaging(currentIndex, pageSize, total)
+	resp := response.AbstractListListResp{
+		List:   viewList,
+		Paging: page,
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+func getAbstractList(keyWord string, tagId int, startSize, pageSize int) (total int, viewList []rag.WechatArticleAbstractView, err error) {
+	if keyWord == `` {
+		var condition string
+		var pars []interface{}
+		condition += fmt.Sprintf(` AND c.%s = ?`, rag.WechatPlatformColumns.Enabled)
+		pars = append(pars, 1)
+
+		if keyWord != "" {
+			condition += fmt.Sprintf(` AND a.%s like ?`, rag.WechatArticleAbstractColumns.Content)
+			pars = append(pars, `%`+keyWord+`%`)
+		}
+
+		if tagId > 0 {
+			condition += fmt.Sprintf(` AND d.%s = ?`, rag.WechatPlatformTagMappingColumns.TagID)
+			pars = append(pars, tagId)
+		}
+
+		obj := new(rag.WechatArticleAbstract)
+		tmpTotal, list, tmpErr := obj.GetPageListByTagAndPlatformCondition(condition, pars, startSize, pageSize)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+		total = tmpTotal
+		viewList = obj.WechatArticleAbstractItem(list)
+	} else {
+		sortMap := map[string]string{
+			"ModifyTime":              "desc",
+			"WechatArticleAbstractId": "desc",
+		}
+
+		obj := new(rag.WechatPlatform)
+		platformList, tmpErr := obj.GetListByCondition(` AND enabled = 1 `, []interface{}{}, 0, 100000)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+		platformIdList := make([]int, 0)
+		for _, v := range platformList {
+			platformIdList = append(platformIdList, v.WechatPlatformId)
+		}
+		tagList := make([]int, 0)
+		if tagId > 0 {
+			tagList = append(tagList, tagId)
+		}
+		tmpTotal, list, tmpErr := elastic.WechatArticleAbstractEsSearch(keyWord, tagList, platformIdList, startSize, pageSize, sortMap)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+		total = int(tmpTotal)
+		if list != nil && len(list) > 0 {
+			viewList = list[0].ToViewList(list)
+		}
+	}
+
+	return
+}
+
+// Del
+// @Title 删除摘要
+// @Description 删除摘要
+// @Param	request	body request.BeachOpAbstractReq true "type json string"
+// @Success 200 Ret=200 新增成功
+// @router /abstract/del [post]
+func (c *AbstractController) Del() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+	var req request.BeachOpAbstractReq
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if len(req.WechatArticleAbstractIdList) <= 0 && !req.IsSelectAll {
+		br.Msg = "请选择摘要"
+		br.IsSendEmail = false
+		return
+	}
+
+	vectorKeyList := make([]string, 0)
+	wechatArticleAbstractIdList := make([]int, 0)
+
+	obj := rag.WechatArticleAbstract{}
+
+	if !req.IsSelectAll {
+		list, err := obj.GetByIdList(req.WechatArticleAbstractIdList)
+		if err != nil {
+			br.Msg = "修改失败"
+			br.ErrMsg = "修改失败,查找问题失败,Err:" + err.Error()
+			if utils.IsErrNoRow(err) {
+				br.Msg = "问题不存在"
+				br.IsSendEmail = false
+			}
+			return
+		}
+		if len(list) > 0 {
+			for _, v := range list {
+				// 有加入到向量库,那么就加入到待删除的向量库list中
+				if v.VectorKey != `` {
+					vectorKeyList = append(vectorKeyList, v.VectorKey)
+				}
+				wechatArticleAbstractIdList = append(wechatArticleAbstractIdList, v.WechatArticleAbstractId)
+			}
+		}
+	} else {
+		notIdMap := make(map[int]bool)
+		for _, v := range req.NotWechatArticleAbstractIdList {
+			notIdMap[v] = true
+		}
+
+		_, list, err := getAbstractList(req.KeyWord, req.TagId, 0, 100000)
+		if err != nil {
+			br.Msg = "修改失败"
+			br.ErrMsg = "修改失败,查找问题失败,Err:" + err.Error()
+			if utils.IsErrNoRow(err) {
+				br.Msg = "问题不存在"
+				br.IsSendEmail = false
+			}
+			return
+		}
+		if len(list) > 0 {
+			for _, v := range list {
+				if notIdMap[v.WechatArticleAbstractId] {
+					continue
+				}
+				// 有加入到向量库,那么就加入到待删除的向量库list中
+				if v.VectorKey != `` {
+					vectorKeyList = append(vectorKeyList, v.VectorKey)
+				}
+				wechatArticleAbstractIdList = append(wechatArticleAbstractIdList, v.WechatArticleAbstractId)
+			}
+		}
+	}
+
+	// 删除向量库
+	err = services.DelLlmDoc(vectorKeyList, wechatArticleAbstractIdList)
+	if err != nil {
+		br.Msg = "删除失败"
+		br.ErrMsg = "删除向量库失败,Err:" + err.Error()
+		return
+	}
+
+	// 删除摘要
+	err = obj.DelByIdList(wechatArticleAbstractIdList)
+	if err != nil {
+		br.Msg = "删除失败"
+		br.ErrMsg = "删除失败,Err:" + err.Error()
+		return
+	}
+
+	// 删除es数据
+	for _, wechatArticleAbstractId := range wechatArticleAbstractIdList {
+		go services.DelEsWechatArticleAbstract(wechatArticleAbstractId)
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = `删除成功`
+}
+
+// VectorDel
+// @Title 删除摘要向量库
+// @Description 删除摘要向量库
+// @Param	request	body request.BeachOpAbstractReq true "type json string"
+// @Success 200 Ret=200 新增成功
+// @router /abstract/vector/del [post]
+func (c *AbstractController) VectorDel() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+	var req request.BeachOpAbstractReq
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if len(req.WechatArticleAbstractIdList) <= 0 && !req.IsSelectAll {
+		br.Msg = "请选择摘要"
+		br.IsSendEmail = false
+		return
+	}
+
+	vectorKeyList := make([]string, 0)
+	wechatArticleAbstractIdList := make([]int, 0)
+
+	obj := rag.WechatArticleAbstract{}
+
+	if !req.IsSelectAll {
+		list, err := obj.GetByIdList(req.WechatArticleAbstractIdList)
+		if err != nil {
+			br.Msg = "修改失败"
+			br.ErrMsg = "修改失败,查找问题失败,Err:" + err.Error()
+			if utils.IsErrNoRow(err) {
+				br.Msg = "问题不存在"
+				br.IsSendEmail = false
+			}
+			return
+		}
+		if len(list) > 0 {
+			for _, v := range list {
+				// 有加入到向量库,那么就加入到待删除的向量库list中
+				if v.VectorKey != `` {
+					vectorKeyList = append(vectorKeyList, v.VectorKey)
+				}
+				wechatArticleAbstractIdList = append(wechatArticleAbstractIdList, v.WechatArticleAbstractId)
+			}
+		}
+	} else {
+		notIdMap := make(map[int]bool)
+		for _, v := range req.NotWechatArticleAbstractIdList {
+			notIdMap[v] = true
+		}
+		_, list, err := getAbstractList(req.KeyWord, req.TagId, 0, 100000)
+		if err != nil {
+			br.Msg = "修改失败"
+			br.ErrMsg = "修改失败,查找问题失败,Err:" + err.Error()
+			if utils.IsErrNoRow(err) {
+				br.Msg = "问题不存在"
+				br.IsSendEmail = false
+			}
+			return
+		}
+		if len(list) > 0 {
+			for _, v := range list {
+				if notIdMap[v.WechatArticleAbstractId] {
+					continue
+				}
+
+				// 有加入到向量库,那么就加入到待删除的向量库list中
+				if v.VectorKey != `` {
+					vectorKeyList = append(vectorKeyList, v.VectorKey)
+				}
+				wechatArticleAbstractIdList = append(wechatArticleAbstractIdList, v.WechatArticleAbstractId)
+			}
+		}
+	}
+
+	// 删除摘要库
+	err = services.DelLlmDoc(vectorKeyList, wechatArticleAbstractIdList)
+	if err != nil {
+		br.Msg = "删除失败"
+		br.ErrMsg = "删除失败,Err:" + err.Error()
+		return
+	}
+
+	// 修改ES数据
+	for _, wechatArticleAbstractId := range wechatArticleAbstractIdList {
+		go services.AddOrEditEsWechatArticleAbstract(wechatArticleAbstractId)
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = `删除成功`
+}
+
+// AddVector
+// @Title 删除摘要向量库
+// @Description 删除摘要向量库
+// @Param	request	body request.BeachOpAbstractReq true "type json string"
+// @Success 200 Ret=200 新增成功
+// @router /abstract/vector/add [post]
+func (c *AbstractController) AddVector() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+	var req request.BeachOpAbstractReq
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if len(req.WechatArticleAbstractIdList) <= 0 && !req.IsSelectAll {
+		br.Msg = "请选择摘要"
+		br.IsSendEmail = false
+		return
+	}
+
+	wechatArticleAbstractIdList := make([]int, 0)
+
+	obj := rag.WechatArticleAbstract{}
+
+	if !req.IsSelectAll {
+		list, err := obj.GetByIdList(req.WechatArticleAbstractIdList)
+		if err != nil {
+			br.Msg = "修改失败"
+			br.ErrMsg = "修改失败,查找问题失败,Err:" + err.Error()
+			if utils.IsErrNoRow(err) {
+				br.Msg = "问题不存在"
+				br.IsSendEmail = false
+			}
+			return
+		}
+		if len(list) > 0 {
+			for _, v := range list {
+				wechatArticleAbstractIdList = append(wechatArticleAbstractIdList, v.WechatArticleAbstractId)
+			}
+		}
+	} else {
+		notIdMap := make(map[int]bool)
+		for _, v := range req.NotWechatArticleAbstractIdList {
+			notIdMap[v] = true
+		}
+
+		_, list, err := getAbstractList(req.KeyWord, req.TagId, 0, 100000)
+		if err != nil {
+			br.Msg = "修改失败"
+			br.ErrMsg = "修改失败,查找问题失败,Err:" + err.Error()
+			if utils.IsErrNoRow(err) {
+				br.Msg = "问题不存在"
+				br.IsSendEmail = false
+			}
+			return
+		}
+		if len(list) > 0 {
+			for _, v := range list {
+				if notIdMap[v.WechatArticleAbstractId] {
+					continue
+				}
+				wechatArticleAbstractIdList = append(wechatArticleAbstractIdList, v.WechatArticleAbstractId)
+			}
+		}
+	}
+
+	for _, wechatArticleAbstractId := range wechatArticleAbstractIdList {
+		cache.AddWechatArticleLlmOpToCache(wechatArticleAbstractId, ``)
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = `添加向量库中,请稍后查看`
+}

+ 186 - 0
controllers/llm/chat_ws_controller.go

@@ -0,0 +1,186 @@
+package llm
+
+import (
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/models/system"
+	"eta/eta_api/services/llm/facade"
+	"eta/eta_api/utils"
+	"eta/eta_api/utils/ws"
+	"fmt"
+	"github.com/gorilla/websocket"
+	"net"
+	"net/http"
+	"strings"
+	"time"
+)
+
+type ChatWsController struct {
+	controllers.BaseAuthController
+}
+
+func (cc *ChatWsController) Prepare() {
+	method := cc.Ctx.Input.Method()
+	uri := cc.Ctx.Input.URI()
+	if method == "GET" {
+		authorization := cc.Ctx.Input.Header("authorization")
+		if authorization == "" {
+			authorization = cc.Ctx.Input.Header("Authorization")
+		}
+		if strings.Contains(authorization, ";") {
+			authorization = strings.Replace(authorization, ";", "$", 1)
+		}
+		if authorization == "" {
+			strArr := strings.Split(uri, "?")
+			for k, v := range strArr {
+				fmt.Println(k, v)
+			}
+			if len(strArr) > 1 {
+				authorization = strArr[1]
+				authorization = strings.Replace(authorization, "Authorization", "authorization", -1)
+			}
+		}
+		if authorization == "" {
+			utils.FileLog.Error("authorization为空,未授权")
+			cc.Ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+		tokenStr := authorization
+		tokenArr := strings.Split(tokenStr, "=")
+		token := tokenArr[1]
+
+		session, err := system.GetSysSessionByToken(token)
+		if err != nil {
+			if utils.IsErrNoRow(err) {
+				utils.FileLog.Error("authorization已过期")
+				cc.Ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
+				return
+			}
+			utils.FileLog.Error("authorization查询用户信息失败")
+			cc.Ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
+			return
+		}
+		if session == nil {
+			utils.FileLog.Error("会话不存在")
+			cc.Ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
+			return
+		}
+		//校验token是否合法
+		// JWT校验Token和Account
+		account := utils.MD5(session.UserName)
+		if !utils.CheckToken(account, token) {
+			utils.FileLog.Error("authorization校验不合法")
+			cc.Ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+		if time.Now().After(session.ExpiredTime) {
+			utils.FileLog.Error("authorization过期法")
+			cc.Ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
+			return
+		}
+		admin, err := system.GetSysUserById(session.SysUserId)
+		if err != nil {
+			if utils.IsErrNoRow(err) {
+				utils.FileLog.Error("权限不够")
+				cc.Ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+				return
+			}
+			utils.FileLog.Error("获取用户信息失败")
+			cc.Ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
+			return
+		}
+		if admin == nil {
+			utils.FileLog.Error("权限不够")
+			cc.Ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+			return
+		}
+		//如果不是启用状态
+		if admin.Enabled != 1 {
+			utils.FileLog.Error("用户被禁用")
+			cc.Ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+			return
+		}
+
+		//接口权限校验
+		roleId := admin.RoleId
+		list, e := system.GetMenuButtonApisByRoleId(roleId)
+		if e != nil {
+			utils.FileLog.Error("接口权限查询出错", e)
+			cc.Ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+			return
+		}
+		var api string
+		for _, v := range list {
+			if v.Api != "" {
+				api += v.Api + "&"
+			}
+		}
+		api += "&" + models.BusinessConfMap["PublicApi"]
+		//处理uri请求,去除前缀和参数
+		api = strings.TrimRight(api, "&")
+		uri = strings.Replace(uri, "/adminapi", "", 1)
+		uris := strings.Split(uri, "?")
+		uri = uris[0]
+		//fmt.Println("uri:", uri)
+		apis := strings.Split(api, "&")
+		apiMap := make(map[string]bool, 0)
+		for _, s := range apis {
+			apiMap[s] = true
+		}
+		if !apiMap[uri] {
+			utils.FileLog.Error("用户无权访问")
+			cc.Ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+			return
+		}
+		cc.SysUser = admin
+	} else {
+		utils.FileLog.Error("请求方法类型错误")
+		cc.Ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
+		return
+	}
+}
+
+// ChatConnect @Title 知识库问答创建对话连接
+// @Description 知识库问答创建对话连接
+// @Success 101 {object} response.ListResp
+// @router /chat/connect [get]
+func (cc *ChatWsController) ChatConnect() {
+	if !ws.Allow(cc.SysUser.AdminId, ws.CONNECT_LIMITER) {
+		utils.FileLog.Error("WebSocket连接太频繁,主动拒绝链接")
+		cc.Ctx.ResponseWriter.WriteHeader(http.StatusTooManyRequests)
+		return
+	}
+	wsCon, err := webSocketHandler(cc.Ctx.ResponseWriter, cc.Ctx.Request)
+	if err != nil {
+		utils.FileLog.Error("WebSocket连接失败:", err)
+		cc.Ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
+		return
+	}
+	facade.AddSession(cc.SysUser.AdminId, wsCon)
+}
+
+// upGrader 用于将HTTP连接升级为WebSocket连接
+var upGrader = websocket.Upgrader{
+	ReadBufferSize:  1024,
+	WriteBufferSize: 1024,
+	CheckOrigin: func(r *http.Request) bool {
+		return true
+	},
+}
+
+// WebSocketHandler 处理WebSocket连接
+func webSocketHandler(w http.ResponseWriter, r *http.Request) (conn *websocket.Conn, err error) {
+	conn, err = upGrader.Upgrade(w, r, nil)
+	if err != nil {
+		utils.FileLog.Error("升级协议失败:WebSocket:%s", err.Error())
+		return
+	}
+	// 获取底层 TCP 连接并设置保活
+	if tcpConn, ok := conn.NetConn().(*net.TCPConn); ok {
+		_ = tcpConn.SetKeepAlive(true)
+		_ = tcpConn.SetKeepAlivePeriod(ws.TcpTimeout)
+		utils.FileLog.Info("TCP KeepAlive 已启用")
+	}
+	_ = conn.SetReadDeadline(time.Now().Add(ws.ReadTimeout))
+	return
+}

+ 51 - 0
controllers/llm/kb_controller.go

@@ -0,0 +1,51 @@
+package llm
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/services/llm/facade"
+)
+
+type KbController struct {
+	controllers.BaseAuthController
+}
+
+// SearchDocs  @Title 搜索知识库文档
+// @Description 搜索知识库文档
+// @Success 101 {object} response.ListResp
+// @router /knowledge_base/searchDocs [post]
+func (kbctrl *KbController) SearchDocs() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		kbctrl.Data["json"] = br
+		kbctrl.ServeJSON()
+	}()
+	sysUser := kbctrl.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req facade.LLMKnowledgeSearch
+	err := json.Unmarshal(kbctrl.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	searchResp, err := facade.LLMKnowledgeBaseSearchDocs(req)
+	if err != nil {
+		br.Msg = "搜索知识库失败"
+		br.ErrMsg = "搜索知识库失败:" + err.Error()
+		return
+	}
+	br.Data = searchResp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}

+ 20 - 0
controllers/llm/llm_http/request.go

@@ -0,0 +1,20 @@
+package llm_http
+
+type LLMQuestionReq struct {
+	Question      string `description:"提问"`
+	KnowledgeBase string `description:"知识库"`
+	SessionId     string `description:"会话ID"`
+}
+
+type UserChatReq struct {
+	ChatId    int    `json:"ChatId"`
+	ChatTitle string `json:"ChatTitle" description:"会话名称"`
+}
+
+type UserChatRecordReq struct {
+	Id           int    `json:"Id"`
+	ChatId       int    `json:"ChatId"`
+	Content      string `json:"Content" description:"会话名称"`
+	ChatUserType string `json:"ChatUserType" description:"用户类型"`
+	SendTime     string `json:"SendTime" description:"发送时间"`
+}

+ 17 - 0
controllers/llm/llm_http/response.go

@@ -0,0 +1,17 @@
+package llm_http
+
+import "eta/eta_api/models/llm"
+
+type UserChatListResp struct {
+	TodayList     []llm.UserLlmChatListViewItem
+	YesterdayList []llm.UserLlmChatListViewItem
+	WeekList      []llm.UserLlmChatListViewItem
+}
+type UserChatResp struct {
+	ChatId    int
+	ChatTitle string
+	SendTime  string
+}
+type UserChatAddResp struct {
+	SendTime string
+}

+ 233 - 0
controllers/llm/question.go

@@ -0,0 +1,233 @@
+package llm
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/models/rag"
+	"eta/eta_api/models/rag/request"
+	"eta/eta_api/models/rag/response"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/paging"
+	"strings"
+	"time"
+)
+
+// QuestionController
+// @Description: 问题库管理
+type QuestionController struct {
+	controllers.BaseAuthController
+}
+
+// List
+// @Title 列表
+// @Description 列表
+// @Param   PageSize   query   int  true       "每页数据条数"
+// @Param   CurrentIndex   query   int  true       "当前页页码,从1开始"
+// @Param   KeyWord   query   string  true       "搜索关键词"
+// @Success 200 {object} []*rag.QuestionListListResp
+// @router /question/list [get]
+func (c *QuestionController) List() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+
+	sysUser := c.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		return
+	}
+	pageSize, _ := c.GetInt("PageSize")
+	currentIndex, _ := c.GetInt("CurrentIndex")
+	keyWord := c.GetString("KeyWord")
+
+	var startSize int
+	if pageSize <= 0 {
+		pageSize = utils.PageSize20
+	}
+	if currentIndex <= 0 {
+		currentIndex = 1
+	}
+	startSize = utils.StartIndex(currentIndex, pageSize)
+
+	var condition string
+	var pars []interface{}
+
+	if keyWord != "" {
+		condition += fmt.Sprintf(` AND %s like ?`, rag.QuestionColumns.QuestionContent)
+		pars = append(pars, `%`+keyWord+`%`)
+	}
+
+	obj := new(rag.Question)
+	total, list, err := obj.GetPageListByCondition(condition, pars, startSize, pageSize)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取失败,Err:" + err.Error()
+		return
+	}
+
+	viewList := obj.ListToViewList(list)
+
+	page := paging.GetPaging(currentIndex, pageSize, total)
+	resp := response.QuestionListListResp{
+		List:   viewList,
+		Paging: page,
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// Add
+// @Title 新增问题
+// @Description 新增问题
+// @Param	request	body request.AddQuestionReq true "type json string"
+// @Success 200 Ret=200 新增成功
+// @router /question/add [post]
+func (c *QuestionController) Add() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+	var req request.AddQuestionReq
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	req.Content = strings.TrimSpace(req.Content)
+	if req.Content == "" {
+		br.Msg = "请输入问题"
+		br.IsSendEmail = false
+		return
+	}
+	item := &rag.Question{
+		QuestionId:      0,
+		QuestionContent: req.Content,
+		Sort:            0,
+		ModifyTime:      time.Now(),
+		CreateTime:      time.Now(),
+	}
+	err = item.Create()
+	if err != nil {
+		br.Msg = "添加失败"
+		br.ErrMsg = "添加失败,Err:" + err.Error()
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = `添加成功`
+}
+
+// Edit
+// @Title 编辑问题
+// @Description 编辑问题
+// @Param	request	body request.EditQuestionReq true "type json string"
+// @Success 200 Ret=200 新增成功
+// @router /question/edit [post]
+func (c *QuestionController) Edit() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+	var req request.EditQuestionReq
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.QuestionId <= 0 {
+		br.Msg = "问题id不能为空"
+		br.IsSendEmail = false
+		return
+	}
+	req.Content = strings.TrimSpace(req.Content)
+	if req.Content == "" {
+		br.Msg = "请输入问题"
+		br.IsSendEmail = false
+		return
+	}
+
+	obj := rag.Question{}
+	item, err := obj.GetByID(req.QuestionId)
+	if err != nil {
+		br.Msg = "修改失败"
+		br.ErrMsg = "修改失败,查找问题失败,Err:" + err.Error()
+		if utils.IsErrNoRow(err) {
+			br.Msg = "问题不存在"
+			br.IsSendEmail = false
+		}
+		return
+	}
+	item.QuestionContent = req.Content
+	item.ModifyTime = time.Now()
+	err = item.Update([]string{"question_content", "modify_time"})
+	if err != nil {
+		br.Msg = "修改失败"
+		br.ErrMsg = "修改失败,Err:" + err.Error()
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = `添加成功`
+}
+
+// Del
+// @Title 删除问题
+// @Description 删除问题
+// @Param	request	body request.EditQuestionReq true "type json string"
+// @Success 200 Ret=200 新增成功
+// @router /question/del [post]
+func (c *QuestionController) Del() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+	var req request.EditQuestionReq
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.QuestionId <= 0 {
+		br.Msg = "问题id不能为空"
+		br.IsSendEmail = false
+		return
+	}
+
+	obj := rag.Question{}
+	item, err := obj.GetByID(req.QuestionId)
+	if err != nil {
+		br.Msg = "修改失败"
+		br.ErrMsg = "修改失败,查找问题失败,Err:" + err.Error()
+		if utils.IsErrNoRow(err) {
+			br.Msg = "问题不存在"
+			br.IsSendEmail = false
+		}
+		return
+	}
+	err = item.Del()
+	if err != nil {
+		br.Msg = "删除失败"
+		br.ErrMsg = "删除失败,Err:" + err.Error()
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = `删除成功`
+}

+ 332 - 0
controllers/llm/user_chat_controller.go

@@ -0,0 +1,332 @@
+package llm
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/controllers/llm/llm_http"
+	"eta/eta_api/models"
+	"eta/eta_api/models/llm"
+	llmService "eta/eta_api/services/llm"
+	"eta/eta_api/utils"
+	"time"
+)
+
+type UserChatController struct {
+	controllers.BaseAuthController
+}
+
+// NewChat @Title 新建对话框
+// @Description 新建对话框
+// @Success 101 {object} response.ListResp
+// @router /chat/new_chat [post]
+func (ucCtrl *UserChatController) NewChat() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		ucCtrl.Data["json"] = br
+		ucCtrl.ServeJSON()
+	}()
+	var req llm_http.UserChatReq
+	err := json.Unmarshal(ucCtrl.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	sysUser := ucCtrl.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	if req.ChatTitle == "" {
+		req.ChatTitle = "新会话"
+	}
+	session := llm.UserLlmChat{
+		UserId:      sysUser.AdminId,
+		CreatedTime: time.Now(),
+		ChatTitle:   req.ChatTitle,
+	}
+	var chatResp = new(llm_http.UserChatResp)
+	chatResp.ChatTitle = req.ChatTitle
+	chatResp.ChatId, err = session.CreateChatSession()
+	chatResp.SendTime = time.Now().Format(utils.FormatDateTime)
+	if err != nil {
+		br.Msg = "创建失败"
+		br.ErrMsg = "创建失败,Err:" + err.Error()
+		return
+	}
+	_ = llmService.AddChatRecord(&llm.UserChatRecordRedis{
+		ChatId:       session.Id,
+		ChatUserType: "user",
+		Content:      req.ChatTitle,
+		SendTime:     chatResp.SendTime,
+	})
+	br.Data = chatResp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "创建成功"
+}
+
+// RenameChat @Title 新建对话框
+// @Description 新建对话框
+// @Success 101 {object} response.ListResp
+// @router /chat/rename_chat [post]
+func (ucCtrl *UserChatController) RenameChat() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		ucCtrl.Data["json"] = br
+		ucCtrl.ServeJSON()
+	}()
+	var req llm_http.UserChatReq
+	err := json.Unmarshal(ucCtrl.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	sysUser := ucCtrl.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	if req.ChatId <= 0 {
+		br.Msg = "非法的对话框Id"
+		br.ErrMsg = "非法的对话框Id"
+		return
+	}
+	if req.ChatTitle == "" {
+		br.Msg = "重命名不能为空"
+		br.ErrMsg = "重命名不能为空"
+		return
+	}
+	session := llm.UserLlmChat{
+		Id:         req.ChatId,
+		UpdateTime: time.Now(),
+		UserId:     sysUser.AdminId,
+		ChatTitle:  req.ChatTitle,
+	}
+	err = session.RenameChatSession()
+	if err != nil {
+		br.Msg = "重命名失败"
+		br.ErrMsg = "重命名失败,Err:" + err.Error()
+		return
+	}
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "重命名成功"
+}
+
+// DeleteChat @Title 删除对话框
+// @Description 删除对话框
+// @Success 101 {object} response.ListResp
+// @router /chat/delete_chat [post]
+func (ucCtrl *UserChatController) DeleteChat() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		ucCtrl.Data["json"] = br
+		ucCtrl.ServeJSON()
+	}()
+	var req llm_http.UserChatReq
+	err := json.Unmarshal(ucCtrl.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	sysUser := ucCtrl.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	if req.ChatId <= 0 {
+		br.Msg = "非法的对话框Id"
+		br.ErrMsg = "非法的对话框Id"
+		return
+	}
+
+	session := llm.UserLlmChat{
+		Id:         req.ChatId,
+		UpdateTime: time.Now(),
+		IsDeleted:  1,
+	}
+	err = session.DeleteChatSession()
+	if err != nil {
+		br.Msg = "删除失败"
+		br.ErrMsg = "删除失败,Err:" + err.Error()
+		return
+	}
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "删除成功"
+}
+
+// GetUserChatList @Title 获取用户对话框列表
+// @Description  获取用户对话框列表
+// @Success 101 {object} response.ListResp
+// @router /chat/user_chat_list [get]
+func (ucCtrl *UserChatController) GetUserChatList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		ucCtrl.Data["json"] = br
+		ucCtrl.ServeJSON()
+	}()
+	sysUser := ucCtrl.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	//周日是0,周六是6
+	weekDay := time.Now().Weekday()
+	offset := int(time.Monday - weekDay)
+	if offset > 0 {
+		offset -= 7
+	}
+	today := time.Now().Format(utils.FormatDate)
+	monDay := time.Now().AddDate(0, 0, offset).Format(utils.FormatDate)
+	yesterday := time.Now().AddDate(0, 0, -1).Format(utils.FormatDate)
+	chatList, err := llm.GetUserChatList(sysUser.AdminId, monDay, time.Now().Format(utils.FormatDate))
+	if err != nil {
+		br.Msg = "获取用户聊天列表失败"
+		br.ErrMsg = "获取用户聊天列表失败,Err:" + err.Error()
+		return
+	}
+	data := new(llm_http.UserChatListResp)
+	data.WeekList = make([]llm.UserLlmChatListViewItem, 0)
+	data.YesterdayList = make([]llm.UserLlmChatListViewItem, 0)
+	data.TodayList = make([]llm.UserLlmChatListViewItem, 0)
+	for _, v := range chatList {
+		list, _ := llmService.GetChatRecordsFromRedis(v.Id)
+		item := llm.CovertItemToView(v)
+		item.RecordCount = len(list)
+		if v.CreatedTime.Format(utils.FormatDate) == today {
+			data.TodayList = append(data.TodayList, item)
+		} else if v.CreatedTime.Format(utils.FormatDate) == yesterday {
+			data.YesterdayList = append(data.YesterdayList, item)
+		} else {
+			data.WeekList = append(data.WeekList, item)
+		}
+	}
+
+	br.Data = data
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取用户聊天列表成功"
+}
+
+// ChatRecordAdd @Title 保存聊天记录
+// @Description 保存聊天记录
+// @Success 101 {object} response.ListResp
+// @router /chat/chat_record_save [post]
+func (ucCtrl *UserChatController) ChatRecordAdd() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		ucCtrl.Data["json"] = br
+		ucCtrl.ServeJSON()
+	}()
+	var req llm_http.UserChatRecordReq
+	err := json.Unmarshal(ucCtrl.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	sysUser := ucCtrl.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	if req.ChatId <= 0 {
+		br.Msg = "非法的对话框Id"
+		br.ErrMsg = "非法的对话框Id"
+		return
+	}
+	if req.Content == "" {
+		br.Msg = "聊天记录不能为空"
+		br.ErrMsg = "聊天记录不能为空"
+		return
+	}
+	if req.Id < 0 {
+		br.Msg = "非法的Id"
+		br.ErrMsg = "非法的Id"
+		return
+	}
+	if req.ChatUserType != "user" && req.ChatUserType != "assistant" {
+		br.Msg = "非法的用户类型"
+		br.ErrMsg = "非法的用户类型,用户类型支持:user/assistant"
+		return
+	}
+	if req.SendTime == "" {
+		req.SendTime = time.Now().Format(utils.FormatDateTime)
+	} else {
+		_, err = time.Parse(utils.FormatDateTime, req.SendTime)
+		if err != nil {
+			br.Msg = "非法的发送时间"
+			br.ErrMsg = "非法的发送时间,Err:" + err.Error()
+			return
+		}
+	}
+	record := llm.UserChatRecordRedis{
+		ChatId:       req.ChatId,
+		ChatUserType: req.ChatUserType,
+		Content:      req.Content,
+		SendTime:     req.SendTime,
+	}
+	err = llmService.AddChatRecord(&record)
+	if err != nil {
+		br.Msg = "添加聊天记录失败"
+		br.ErrMsg = "添加聊天记录失败,Err:" + err.Error()
+		return
+	}
+
+	br.Data = llm_http.UserChatAddResp{
+		SendTime: record.SendTime,
+	}
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "添加聊天记录成功"
+}
+
+// ChatRecordList @Title 获取聊天记录
+// @Description 获取聊天记录
+// @Success 101 {object} response.ListResp
+// @router /chat/chat_record_list [get]
+func (ucCtrl *UserChatController) ChatRecordList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		ucCtrl.Data["json"] = br
+		ucCtrl.ServeJSON()
+	}()
+	sysUser := ucCtrl.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	chatId, _ := ucCtrl.GetInt("ChatId", 0)
+	if chatId <= 0 {
+		br.Msg = "非法的对话Id"
+		br.ErrMsg = "非法的对话Id"
+		return
+	}
+
+	list, err := llmService.GetChatRecordsFromRedis(chatId)
+	if err != nil {
+		br.Msg = "获取聊天记录失败"
+		br.ErrMsg = "获取聊天记录失败,Err:" + err.Error()
+		return
+	}
+	br.Data = list
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取聊天记录成功"
+}

+ 777 - 0
controllers/llm/wechat_platform.go

@@ -0,0 +1,777 @@
+package llm
+
+import (
+	"encoding/json"
+	"eta/eta_api/cache"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/models/rag"
+	"eta/eta_api/models/rag/request"
+	"eta/eta_api/models/rag/response"
+	"eta/eta_api/models/system"
+	"eta/eta_api/services"
+	"eta/eta_api/services/elastic"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/paging"
+	"html"
+	"strings"
+	"time"
+)
+
+// WechatPlatformController
+// @Description: 微信公众号管理
+type WechatPlatformController struct {
+	controllers.BaseAuthController
+}
+
+// TagList
+// @Title 获取ppt列表
+// @Description 获取ppt列表接口
+// @Param   PageSize   query   int  true       "每页数据条数"
+// @Param   CurrentIndex   query   int  true       "当前页页码,从1开始"
+// @Param   KeyWord   query   string  true       "搜索关键词"
+// @Success 200 {object} models.TagListResp
+// @router /tag/list [get]
+func (c *WechatPlatformController) TagList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+
+	sysUser := c.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		return
+	}
+	pageSize, _ := c.GetInt("PageSize")
+	currentIndex, _ := c.GetInt("CurrentIndex")
+	keyWord := c.GetString("KeyWord")
+
+	var startSize int
+	if pageSize <= 0 {
+		pageSize = utils.PageSize20
+	}
+	if currentIndex <= 0 {
+		currentIndex = 1
+	}
+	startSize = utils.StartIndex(currentIndex, pageSize)
+
+	var condition string
+	var pars []interface{}
+
+	if keyWord != "" {
+		condition += fmt.Sprintf(` AND %s like ?`, rag.WechatPlatformColumns.Nickname)
+		pars = append(pars, `%`+keyWord+`%`)
+	}
+
+	obj := new(rag.Tag)
+	total, list, err := obj.GetPageListByCondition(condition, pars, startSize, pageSize)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取失败,Err:" + err.Error()
+		return
+	}
+
+	page := paging.GetPaging(currentIndex, pageSize, total)
+	resp := new(response.TagListResp)
+	resp.Paging = page
+	resp.List = list
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// Add
+// @Title 新增公众号
+// @Description 新增公众号
+// @Param	request	body request.AddWechatPlatformReq true "type json string"
+// @Success 200 Ret=200 新增成功
+// @router /wechat_platform/add [post]
+func (c *WechatPlatformController) Add() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+	var req request.AddWechatPlatformReq
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	req.Name = strings.TrimSpace(req.Name)
+	if req.Name == "" {
+		br.Msg = "请输入公众号名称"
+		br.IsSendEmail = false
+		return
+	}
+	req.Link = strings.TrimSpace(req.Link)
+	if req.Link == "" {
+		br.Msg = "请输入文章链接"
+		br.IsSendEmail = false
+		return
+	}
+
+	// 名称校验
+	{
+		var condition string
+		var pars []interface{}
+		condition += fmt.Sprintf(` AND %s = ?`, rag.WechatPlatformColumns.Nickname)
+		pars = append(pars, req.Name)
+		obj := new(rag.WechatPlatform)
+		item, err := obj.GetByCondition(condition, pars)
+		if err != nil && !utils.IsErrNoRow(err) {
+			br.Msg = "公众号信息获取失败"
+			br.ErrMsg = "公众号信息获取失败,Err:" + err.Error()
+			return
+		}
+
+		if item.WechatPlatformId > 0 {
+			br.Msg = "公众号名称重复"
+			br.IsSendEmail = false
+			return
+		}
+	}
+
+	// 链接校验
+	{
+		var condition string
+		var pars []interface{}
+		condition += fmt.Sprintf(` AND %s = ?`, rag.WechatPlatformColumns.ArticleLink)
+		pars = append(pars, req.Link)
+		obj := new(rag.WechatPlatform)
+		item, err := obj.GetByCondition(condition, pars)
+		if err != nil && !utils.IsErrNoRow(err) {
+			br.Msg = "公众号信息获取失败"
+			br.ErrMsg = "公众号信息获取失败,Err:" + err.Error()
+			return
+		}
+
+		if item.WechatPlatformId > 0 {
+			br.Msg = "公众号链接重复"
+			br.IsSendEmail = false
+			return
+		}
+	}
+
+	item := &rag.WechatPlatform{
+		WechatPlatformId: 0,
+		FakeId:           "",
+		Nickname:         req.Name,
+		Alias:            "",
+		RoundHeadImg:     "",
+		ServiceType:      0,
+		Signature:        "",
+		Verified:         0,
+		ArticleLink:      req.Link,
+		Enabled:          0,
+		SysUserId:        c.SysUser.AdminId,
+		ModifyTime:       time.Now(),
+		CreateTime:       time.Now(),
+	}
+	err = item.Add(req.TagIdList)
+	if err != nil {
+		br.Msg = "添加失败"
+		br.ErrMsg = "添加失败,Err:" + err.Error()
+		return
+	}
+
+	// 异步新增公众号
+	go cache.AddWechatArticleOpToCache(item.WechatPlatformId, `add`)
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = `添加成功`
+}
+
+// FollowList
+// @Title 我关注的接口
+// @Description 我关注的接口
+// @Param   PageSize   query   int  true       "每页数据条数"
+// @Param   CurrentIndex   query   int  true       "当前页页码,从1开始"
+// @Param   KeyWord   query   string  true       "搜索关键词"
+// @Success 200 {object} []*rag.WechatPlatform
+// @router /wechat_platform/list/follow [get]
+func (c *WechatPlatformController) FollowList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+
+	sysUser := c.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		return
+	}
+	pageSize, _ := c.GetInt("PageSize")
+	currentIndex, _ := c.GetInt("CurrentIndex")
+	keyWord := c.GetString("KeyWord")
+
+	var startSize int
+	if pageSize <= 0 {
+		pageSize = utils.PageSize20
+	}
+	if currentIndex <= 0 {
+		currentIndex = 1
+	}
+	startSize = utils.StartIndex(currentIndex, pageSize)
+
+	var condition string
+	var pars []interface{}
+
+	if keyWord != "" {
+		condition += fmt.Sprintf(` AND %s like ?`, rag.WechatPlatformColumns.Nickname)
+		pars = append(pars, `%`+keyWord+`%`)
+	}
+
+	condition += fmt.Sprintf(` AND b.%s = ?`, rag.WechatPlatformUserMappingColumns.SysUserID)
+	pars = append(pars, c.SysUser.AdminId)
+
+	obj := new(rag.WechatPlatformUserMapping)
+	list, err := obj.GetListByCondition(condition, pars, startSize, pageSize)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取失败,Err:" + err.Error()
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = list
+}
+
+// PublicList
+// @Title 公共列表
+// @Description 公共列表
+// @Param   PageSize   query   int  true       "每页数据条数"
+// @Param   CurrentIndex   query   int  true       "当前页页码,从1开始"
+// @Param   KeyWord   query   string  true       "搜索关键词"
+// @Success 200 {object} models.WechatPlatformListResp
+// @router /wechat_platform/list/public [get]
+func (c *WechatPlatformController) PublicList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+
+	sysUser := c.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		return
+	}
+	pageSize, _ := c.GetInt("PageSize")
+	currentIndex, _ := c.GetInt("CurrentIndex")
+	keyWord := c.GetString("KeyWord")
+
+	var startSize int
+	if pageSize <= 0 {
+		pageSize = utils.PageSize20
+	}
+	if currentIndex <= 0 {
+		currentIndex = 1
+	}
+	startSize = utils.StartIndex(currentIndex, pageSize)
+
+	var condition string
+	var pars []interface{}
+
+	if keyWord != "" {
+		condition += fmt.Sprintf(` AND %s like ?`, rag.WechatPlatformColumns.Nickname)
+		pars = append(pars, `%`+keyWord+`%`)
+	}
+
+	obj := new(rag.WechatPlatformUserMapping)
+	list, err := obj.GetListByCondition(condition, pars, startSize, 100000)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取失败,Err:" + err.Error()
+		return
+	}
+
+	resp := make([]response.WechatPlatformPublicListResp, 0)
+
+	if list != nil && len(list) > 0 {
+		userIdList := make([]int, 0)
+		uerIdMap := make(map[int]bool)
+		userFollowIndexMap := make(map[int]int)
+		for _, v := range list {
+			if _, ok := uerIdMap[v.FollowUserId]; !ok {
+				userIdList = append(userIdList, v.FollowUserId)
+				uerIdMap[v.FollowUserId] = true
+			}
+
+			index, ok := userFollowIndexMap[v.FollowUserId]
+			if !ok {
+				userFollowIndexMap[v.FollowUserId] = len(resp)
+
+				resp = append(resp, response.WechatPlatformPublicListResp{
+					UserId: v.FollowUserId,
+					List:   []*rag.UserFollowWechatPlatform{v},
+				})
+			} else {
+				resp[index].List = append(resp[index].List, v)
+			}
+		}
+
+		userList, err := system.GetAdminListByIdList(userIdList)
+		if err != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取失败,Err:" + err.Error()
+			return
+		}
+		userNameMap := make(map[int]*system.Admin)
+		for _, v := range userList {
+			userNameMap[v.AdminId] = v
+		}
+
+		for k, v := range resp {
+			userInfo, ok := userNameMap[v.UserId]
+			if !ok {
+				continue
+			}
+			resp[k].Name = userInfo.RealName + `关注`
+		}
+
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// PublicList
+// @Title 公共列表
+// @Description 公共列表
+// @Param   PageSize   query   int  true       "每页数据条数"
+// @Param   CurrentIndex   query   int  true       "当前页页码,从1开始"
+// @Param   KeyWord   query   string  true       "搜索关键词"
+// @Success 200 {object} models.WechatPlatformListResp
+// @router /wechat_platform/op [post]
+func (c *WechatPlatformController) Op() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+
+	sysUser := c.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		return
+	}
+
+	var req request.OpWechatPlatformReq
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.WechatPlatformId <= 0 {
+		br.Msg = "参数错误"
+		return
+	}
+
+	if req.Status < 0 || req.Status > 1 {
+		br.Msg = "参数错误"
+		return
+	}
+	obj := rag.WechatPlatform{}
+	wechatPlatform, err := obj.GetById(req.WechatPlatformId)
+	if err != nil {
+		br.Msg = "修改失败"
+		br.ErrMsg = "修改失败,Err:" + err.Error()
+		if utils.IsErrNoRow(err) {
+			br.Msg = "公众号不存在"
+			br.IsSendEmail = false
+		}
+		return
+	}
+	wechatPlatform.Enabled = req.Status
+	wechatPlatform.ModifyTime = time.Now()
+	err = wechatPlatform.Update([]string{"enabled", `modify_time`})
+	if err != nil {
+		br.Msg = "修改失败"
+		br.ErrMsg = "修改失败,Err:" + err.Error()
+		return
+	}
+
+	// 异步处理公众号下面的文章
+	go services.AddOrEditEsWechatPlatformId(wechatPlatform.WechatPlatformId)
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "修改成功"
+}
+
+// Refresh
+// @Title 公共列表
+// @Description 公共列表
+// @Success 200 {object} models.WechatPlatformListResp
+// @router /wechat_platform/refresh [post]
+func (c *WechatPlatformController) Refresh() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+
+	sysUser := c.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		return
+	}
+
+	var req request.RefreshWechatPlatformReq
+	err := json.Unmarshal(c.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.WechatPlatformId <= 0 {
+		br.Msg = "参数错误"
+		return
+	}
+
+	obj := rag.WechatPlatform{}
+	wechatPlatform, err := obj.GetById(req.WechatPlatformId)
+	if err != nil {
+		br.Msg = "修改失败"
+		br.ErrMsg = "修改失败,Err:" + err.Error()
+		if utils.IsErrNoRow(err) {
+			br.Msg = "公众号不存在"
+			br.IsSendEmail = false
+		}
+		return
+	}
+	if wechatPlatform.FakeId != `` {
+		br.Msg = "公众号已添加成功"
+		br.ErrMsg = "公众号已添加成功"
+		br.IsSendEmail = false
+		return
+	}
+
+	go cache.AddWechatArticleOpToCache(wechatPlatform.WechatPlatformId, `add`)
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "刷新成功"
+}
+
+// ArticleList
+// @Title 我关注的接口
+// @Description 我关注的接口
+// @Param   PageSize   query   int  true       "每页数据条数"
+// @Param   CurrentIndex   query   int  true       "当前页页码,从1开始"
+// @Param   WechatPlatformId   query   int  true       "微信公众号id"
+// @Param   KeyWord   query   string  true       "搜索关键词"
+// @Success 200 {object} []*rag.WechatPlatform
+// @router /wechat_platform/article/list [get]
+func (c *WechatPlatformController) ArticleList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+
+	sysUser := c.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		return
+	}
+	pageSize, _ := c.GetInt("PageSize")
+	currentIndex, _ := c.GetInt("CurrentIndex")
+	wechatPlatformId, _ := c.GetInt("WechatPlatformId")
+	keyWord := c.GetString("KeyWord")
+
+	var startSize int
+	if pageSize <= 0 {
+		pageSize = utils.PageSize20
+	}
+	if currentIndex <= 0 {
+		currentIndex = 1
+	}
+	startSize = utils.StartIndex(currentIndex, pageSize)
+
+	//var condition string
+	//var pars []interface{}
+	//
+	//if keyWord != "" {
+	//	condition += fmt.Sprintf(` AND (b.%s like ? or a.%s like ? ) `, rag.WechatPlatformColumns.Nickname, rag.WechatArticleColumns.Title)
+	//	pars = append(pars, `%`+keyWord+`%`, `%`+keyWord+`%`)
+	//}
+	//
+	//if wechatPlatformId > 0 {
+	//	condition += fmt.Sprintf(` AND a.%s = ?`, rag.WechatArticleColumns.WechatPlatformID)
+	//	pars = append(pars, wechatPlatformId)
+	//}
+	//
+	//condition += fmt.Sprintf(` AND b.%s = ? `, rag.WechatPlatformColumns.Enabled)
+	//pars = append(pars, 1)
+
+	var total int
+	viewList := make([]rag.WechatArticleView, 0)
+
+	if keyWord == `` {
+		var condition string
+		var pars []interface{}
+
+		if keyWord != "" {
+			condition += fmt.Sprintf(` AND (b.%s like ? or a.%s like ? ) `, rag.WechatPlatformColumns.Nickname, rag.WechatArticleColumns.Title)
+			pars = append(pars, `%`+keyWord+`%`, `%`+keyWord+`%`)
+		}
+
+		if wechatPlatformId > 0 {
+			condition += fmt.Sprintf(` AND a.%s = ?`, rag.WechatArticleColumns.WechatPlatformID)
+			pars = append(pars, wechatPlatformId)
+		}
+
+		//condition += fmt.Sprintf(` AND b.%s = ? `, rag.WechatPlatformColumns.Enabled)
+		//pars = append(pars, 1)
+
+		obj := new(rag.WechatArticle)
+		tmpTotal, list, err := obj.GetPageListByPlatformCondition(condition, pars, startSize, pageSize)
+		if err != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取失败,Err:" + err.Error()
+			return
+		}
+		total = tmpTotal
+
+		if list != nil && len(list) > 0 {
+			viewList = obj.ArticleAndPlatformListToViewList(list)
+		}
+	} else {
+		sortMap := map[string]string{
+			"ArticleCreateTime": "desc",
+			"WechatArticleId":   "desc",
+		}
+		tmpTotal, list, err := elastic.WechatArticleEsSearch(keyWord, wechatPlatformId, startSize, pageSize, sortMap)
+		if err != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取失败,Err:" + err.Error()
+			return
+		}
+		total = int(tmpTotal)
+		if list != nil && len(list) > 0 {
+			viewList = list[0].ArticleAndPlatformListToViewList(list)
+		}
+	}
+
+	page := paging.GetPaging(currentIndex, pageSize, total)
+	resp := response.WechatArticleListListResp{
+		List:   viewList,
+		Paging: page,
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// ArticleList
+// @Title 我关注的接口
+// @Description 我关注的接口
+// @Param   WechatArticleId   query   int  true       "文章id"
+// @Success 200 {object} []*rag.WechatArticle
+// @router /wechat_platform/article/detail [get]
+func (c *WechatPlatformController) ArticleDetail() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+
+	sysUser := c.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		return
+	}
+	wechatArticleId, _ := c.GetInt("WechatArticleId")
+	if wechatArticleId <= 0 {
+		br.Msg = "请选择文章"
+		br.IsSendEmail = false
+		return
+	}
+	obj := new(rag.WechatArticle)
+	item, err := obj.GetById(wechatArticleId)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取失败,Err:" + err.Error()
+		return
+	}
+
+	if item.IsDeleted == 1 {
+		br.Msg = "文章已删除"
+		br.IsSendEmail = false
+		return
+	}
+	resp := item.ToView()
+	resp.Content = html.UnescapeString(item.Content)
+
+	// 获取摘要信息
+	{
+		abstractObj := rag.WechatArticleAbstract{}
+		abstractItem, err := abstractObj.GetByWechatArticleId(wechatArticleId)
+		if err != nil && !utils.IsErrNoRow(err) {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取失败,Err:" + err.Error()
+			return
+		}
+		resp.Abstract = abstractItem.Content
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = resp
+}
+
+// ArticleList
+// @Title 我关注的接口
+// @Description 我关注的接口
+// @Param   WechatArticleId   query   int  true       "文章id"
+// @Success 200 {object} []*rag.WechatPlatform
+// @router /wechat_platform/article/del [get]
+func (c *WechatPlatformController) ArticleDel() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		c.Data["json"] = br
+		c.ServeJSON()
+	}()
+
+	sysUser := c.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		return
+	}
+	wechatArticleId, _ := c.GetInt("WechatArticleId")
+	if wechatArticleId <= 0 {
+		br.Msg = "请选择文章"
+		br.IsSendEmail = false
+		return
+	}
+	obj := new(rag.WechatArticle)
+	item, err := obj.GetById(wechatArticleId)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取失败,Err:" + err.Error()
+		return
+	}
+
+	if item.IsDeleted == 1 {
+		br.Msg = "文章已删除"
+		br.IsSendEmail = false
+		return
+	}
+	item.IsDeleted = 1
+	err = item.Update([]string{"is_deleted"})
+	if err != nil {
+		br.Msg = "删除失败"
+		br.ErrMsg = "删除失败,Err:" + err.Error()
+		return
+	}
+
+	// 修改ES信息
+	services.AddOrEditEsWechatArticle(item.WechatArticleId)
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "删除成功"
+}
+
+//func init() {
+//	//obj := rag.WechatPlatform{}
+//	//item, _ := obj.GetByID(2)
+//	//fmt.Println(llm.BeachAddWechatPlatform(item))
+//
+//	obj := rag.WechatArticle{}
+//	//item, _ := obj.GetById(30)
+//	list, _ := obj.GetListByCondition(``, ` `, []interface{}{}, 0, 1000)
+//	//llm.ArticleToTmpFile(item.TextContent)
+//	for _, item := range list {
+//		//llm.ArticleToKnowledge(item)
+//		llm.GenerateArticleAbstract(item)
+//	}
+//}
+
+//func init() {
+//	//obj := rag.WechatPlatform{}
+//	//item, _ := obj.GetByID(2)
+//	//fmt.Println(llm.BeachAddWechatPlatform(item))
+//
+//	//obj := rag.WechatArticle{}
+//	//list, _ := obj.GetListByCondition(`wechat_article_id,content`, `  `, []interface{}{}, 0, 1)
+//	////obj := rag.WechatPlatform{}
+//	////list, _ := obj.GetListByCondition(` AND wechat_platform_id !=1 `, []interface{}{}, 0, 100)
+//	////llm.ArticleToTmpFile(item.TextContent)
+//	//for _, item := range list {
+//	//	//llm.ArticleToKnowledge(item)
+//	//	services.ReGenerateArticleAbstract(item)
+//	//}
+//
+//	// 重新生成摘要
+//	{
+//		obj := rag.WechatArticle{}
+//		list, _ := obj.GetListByCondition(``, ` AND text_content !='' AND abstract_status = 0`, []interface{}{}, 0, 300)
+//		for _, item := range list {
+//			services.GenerateArticleAbstract(item)
+//		}
+//	}
+//
+//	//// 删除摘要向量库
+//	//{
+//	//	obj := rag.WechatArticleAbstract{}
+//	//	list, _ := obj.GetListByCondition(`vector_key,wechat_article_abstract_id`, ` AND wechat_article_id in (25,27) `, []interface{}{}, 0, 10000)
+//	//	fmt.Println(services.DelDoc(list))
+//	//}
+//
+//	fmt.Println("修复结束")
+//}
+
+//func init() {
+//	//// 微信文章加到es
+//	//{
+//	//	obj := rag.WechatArticle{}
+//	//	list, _ := obj.GetListByCondition(` wechat_article_id `, ` `, []interface{}{}, 0, 10000)
+//	//	total := len(list)
+//	//	for k, item := range list {
+//	//		fmt.Println(k, "/", total)
+//	//		services.AddOrEditEsWechatArticle(item.WechatArticleId)
+//	//	}
+//	//
+//	//	fmt.Println("结束了")
+//	//}
+//
+//	//// 微信文章加到es
+//	{
+//		obj := rag.WechatArticleAbstract{}
+//		list, _ := obj.GetListByCondition(` wechat_article_abstract_id `, ` `, []interface{}{}, 0, 10000)
+//		total := len(list)
+//		for k, item := range list {
+//			fmt.Println(k, "/", total)
+//			services.AddOrEditEsWechatArticleAbstract(item.WechatArticleAbstractId)
+//		}
+//
+//		fmt.Println("结束了")
+//	}
+//
+//}

+ 5 - 1
controllers/sys_role.go

@@ -712,7 +712,11 @@ func (this *SysRoleController) SystemConfig() {
 	}, system.BusinessConf{
 		ConfKey: "LoginUrl",
 		ConfVal: conf["LoginUrl"],
-	})
+	},
+        system.BusinessConf{
+        ConfKey: "KnowledgeBaseName",
+    	ConfVal: conf["KnowledgeBaseName"],
+    })
 
 	osc := system.BusinessConf{
 		ConfKey: "ObjectStorageClient",

+ 1 - 0
controllers/user_login.go

@@ -560,6 +560,7 @@ func (this *UserLoginController) Login() {
 	}
 	resp.ProductName = productName
 	resp.Authority = sysUser.Authority
+	resp.Mobile = sysUser.Mobile
 
 	// 设置redis缓存
 	{

+ 0 - 2
main.go

@@ -12,7 +12,6 @@ import (
 	_ "eta/eta_api/routers"
 	"eta/eta_api/services"
 	"eta/eta_api/utils"
-
 	"github.com/beego/beego/v2/adapter/logs"
 	"github.com/beego/beego/v2/server/web"
 	"github.com/beego/beego/v2/server/web/context"
@@ -23,7 +22,6 @@ func main() {
 		web.BConfig.WebConfig.DirectoryIndex = true
 		web.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
 	}
-
 	go services.Task()
 
 	// 异常处理

+ 31 - 0
models/business_conf.go

@@ -1,6 +1,7 @@
 package models
 
 import (
+	"encoding/json"
 	"eta/eta_api/global"
 	"eta/eta_api/utils"
 	"fmt"
@@ -56,6 +57,11 @@ const (
 	BusinessConfReportViewUrl                = "ReportViewUrl"                // 报告详情地址
 	BusinessConfEsIndexNameExcel             = "EsIndexNameExcel"             // ES索引名称-表格
 	BusinessConfEsIndexNameDataSource        = "EsIndexNameDataSource"        // ES索引名称-数据源
+	LLMInitConfig                            = "llmInitConfig"
+	KnowledgeBaseName                        = "KnowledgeBaseName"                // 摘要库
+	KnowledgeArticleName                     = "KnowledgeArticleName"             // 原文库
+	BusinessConfEsWechatArticle              = "EsIndexNameWechatArticle"         // ES索引名称-微信文章
+	BusinessConfEsWechatArticleAbstract      = "EsIndexNameWechatArticleAbstract" // ES索引名称-微信文章摘要
 )
 
 const (
@@ -63,6 +69,7 @@ const (
 	BusinessConfReportApproveTypeOther = "other"
 	BusinessConfClientFlagNanHua       = "nhqh" // 南华标记
 	BusinessConfEmailClientSmtp        = "smtp" // 普通邮箱标记
+
 )
 
 // FromSceneMap 数据源名称与数据源ID的对应关系
@@ -241,6 +248,11 @@ func InitUseMongoConf() {
 	}
 }
 
+type LLMConfig struct {
+	LlmAddress string `json:"llm_server"`
+	LlmModel   string `json:"llm_model"`
+}
+
 func InitBusinessConf() {
 	var e error
 	BusinessConfMap, e = GetBusinessConf()
@@ -254,4 +266,23 @@ func InitBusinessConf() {
 	if BusinessConfMap[BusinessConfEsIndexNameDataSource] != "" {
 		utils.EsDataSourceIndexName = BusinessConfMap[BusinessConfEsIndexNameDataSource]
 	}
+	// ES索引名称
+	if BusinessConfMap[BusinessConfEsWechatArticle] != "" {
+		utils.EsWechatArticleName = BusinessConfMap[BusinessConfEsWechatArticle]
+	}
+	if BusinessConfMap[BusinessConfEsWechatArticleAbstract] != "" {
+		utils.EsWechatArticleAbstractName = BusinessConfMap[BusinessConfEsWechatArticleAbstract]
+	}
+	confStr := BusinessConfMap[LLMInitConfig]
+	if confStr != "" {
+		var config LLMConfig
+		err := json.Unmarshal([]byte(confStr), &config)
+		if err != nil {
+			utils.FileLog.Error("LLM配置错误")
+		}
+
+		utils.LLM_MODEL = config.LlmModel
+		utils.LLM_SERVER = config.LlmAddress
+	}
+
 }

+ 40 - 0
models/llm/user_chat_record.go

@@ -0,0 +1,40 @@
+package llm
+
+import (
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"time"
+)
+
+// UserChatRecord 定义用户聊天记录结构体
+type UserChatRecord struct {
+	Id           int       `gorm:"primaryKey;autoIncrement;comment:主键"`
+	ChatId       int       `gorm:"chat_id;comment:会话id"`
+	ChatUserType string    `gorm:"type:enum('user','assistant');comment:用户方"`
+	Content      string    `gorm:"content:内容"`
+	SendTime     time.Time `gorm:"comment:发送时间"`
+	CreatedTime  time.Time `gorm:"comment:创建时间"`
+	UpdateTime   time.Time `gorm:"autoUpdateTime;comment:更新时间"`
+}
+type UserChatRecordRedis struct {
+	Id           int
+	ChatId       int
+	ChatUserType string
+	Content      string
+	SendTime     string
+}
+
+func (u *UserChatRecord) TableName() string {
+	return "user_chat_record"
+}
+
+func BatchInsertRecords(list []*UserChatRecord) (err error) {
+	o := global.DbMap[utils.DbNameAI]
+	err = o.Clauses(clause.OnConflict{
+		Columns:   []clause.Column{{Name: "chat_id"}, {Name: "chat_user_type"}, {Name: "send_time"}},
+		DoUpdates: clause.Assignments(map[string]interface{}{"update_time": gorm.Expr("VALUES(update_time)")}),
+	}).CreateInBatches(list, utils.MultiAddNum).Error
+	return
+}

+ 73 - 0
models/llm/user_llm_chat.go

@@ -0,0 +1,73 @@
+package llm
+
+import (
+	"errors"
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"time"
+)
+
+type UserLlmChat struct {
+	Id          int       `gorm:"primaryKey;autoIncrement;comment:会话主键"`
+	UserId      int       `gorm:"comment:用户id"`
+	ChatTitle   string    `gorm:"comment:会话标题"`
+	IsDeleted   int       `gorm:"comment:是否删除"`
+	CreatedTime time.Time `gorm:"comment:创建时间"`
+	UpdateTime  time.Time `gorm:"autoUpdateTime;comment:更新时间"`
+}
+
+
+type UserLlmChatListViewItem struct {
+	Id          int    `gorm:"primaryKey;autoIncrement;comment:会话主键"`
+	UserId      int    `gorm:"comment:用户id"`
+	ChatTitle   string `gorm:"comment:会话标题"`
+	CreatedTime string `gorm:"comment:创建时间"`
+	RecordCount int    `gorm:"comment:会话记录数"`
+}
+
+func CovertItemToView(item UserLlmChat) UserLlmChatListViewItem {
+	return UserLlmChatListViewItem{
+		Id:          item.Id,
+		UserId:      item.UserId,
+		ChatTitle:   item.ChatTitle,
+		CreatedTime: item.CreatedTime.Format(utils.FormatDateTime),
+	}
+
+}
+func (u *UserLlmChat) TableName() string {
+	return "user_llm_chat"
+}
+func (u *UserLlmChat) CreateChatSession() (chatId int, err error) {
+	o := global.DbMap[utils.DbNameAI]
+	err = o.Create(u).Error
+	if err != nil {
+		return
+	}
+	chatId = u.Id
+	return
+}
+func (u *UserLlmChat) RenameChatSession() (err error) {
+	o := global.DbMap[utils.DbNameAI]
+	var exists bool
+	err = o.Model(&u).Select("1").Where("id = ?", u.Id).Scan(&exists).Error
+	if err != nil {
+		return
+	}
+	if !exists {
+		err = errors.New("当前会话不存在")
+		return
+	}
+	err = o.Select("chat_title").Updates(u).Error
+	return
+}
+func (u *UserLlmChat) DeleteChatSession() (err error) {
+	o := global.DbMap[utils.DbNameAI]
+	err = o.Select("is_deleted").Updates(u).Error
+	return
+}
+func GetUserChatList(userId int, monDay, toDay string) (chatList []UserLlmChat, err error) {
+	o := global.DbMap[utils.DbNameAI]
+	sql := `select ulc.id AS id ,ulc.user_id as user_id,ulc.chat_title as chat_title,ulc.created_time from user_llm_chat ulc  where ulc.user_id=? and ` + utils.GenerateQuerySql(utils.ToDate, &utils.QueryParam{Column: "ulc.created_time"}) + ` BETWEEN ? and ? AND is_deleted=0 GROUP BY ulc.id order by ulc.created_time desc`
+	err = o.Raw(sql, userId, monDay, toDay).Find(&chatList).Error
+	return
+}

+ 135 - 0
models/rag/question.go

@@ -0,0 +1,135 @@
+package rag
+
+import (
+	"database/sql"
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"time"
+)
+
+type Question struct {
+	QuestionId      int       `gorm:"column:question_id;type:int(9) UNSIGNED;primaryKey;not null;" description:"question_id"`
+	QuestionContent string    `gorm:"column:question_content;type:varchar(255);comment:问题内容;" description:"问题内容"`
+	Sort            int       `gorm:"column:sort;type:int(11);comment:排序;default:0;" description:"排序"`
+	ModifyTime      time.Time `gorm:"column:modify_time;type:datetime;default:NULL;" description:"modify_time"`
+	CreateTime      time.Time `gorm:"column:create_time;type:datetime;default:NULL;" description:"create_time"`
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *Question) TableName() string {
+	return "question"
+}
+
+// QuestionColumns get sql column name.获取数据库列名
+var QuestionColumns = struct {
+	QuestionID      string
+	QuestionContent string
+	Sort            string
+	ModifyTime      string
+	CreateTime      string
+}{
+	QuestionID:      "question_id",
+	QuestionContent: "question_content",
+	Sort:            "sort",
+	ModifyTime:      "modify_time",
+	CreateTime:      "create_time",
+}
+
+func (m *Question) Create() (err error) {
+	err = global.DbMap[utils.DbNameAI].Create(&m).Error
+
+	return
+}
+
+func (m *Question) Update(updateCols []string) (err error) {
+	err = global.DbMap[utils.DbNameAI].Select(updateCols).Updates(&m).Error
+
+	return
+}
+
+func (m *Question) Del() (err error) {
+	err = global.DbMap[utils.DbNameAI].Delete(&m).Error
+
+	return
+}
+
+type QuestionView struct {
+	QuestionId      int    `gorm:"column:question_id;type:int(9) UNSIGNED;primaryKey;not null;" description:"question_id"`
+	QuestionContent string `gorm:"column:question_content;type:varchar(255);comment:问题内容;" description:"问题内容"` //
+	Sort            int    `gorm:"column:sort;type:int(11);comment:排序;default:0;" description:"sort"`          // 排序
+	ModifyTime      string `gorm:"column:modify_time;type:datetime;default:NULL;" description:"modify_time"`
+	CreateTime      string `gorm:"column:create_time;type:datetime;default:NULL;" description:"create_time"`
+}
+
+func (m *Question) ToView() QuestionView {
+	var modifyTime, createTime string
+
+	if !m.CreateTime.IsZero() {
+		createTime = m.CreateTime.Format(utils.FormatDateTime)
+	}
+	if !m.ModifyTime.IsZero() {
+		modifyTime = m.ModifyTime.Format(utils.FormatDateTime)
+	}
+	return QuestionView{
+		QuestionId:      m.QuestionId,
+		QuestionContent: m.QuestionContent,
+		Sort:            m.Sort,
+		ModifyTime:      modifyTime,
+		CreateTime:      createTime,
+	}
+}
+
+func (m *Question) ListToViewList(list []*Question) (wechatArticleViewList []QuestionView) {
+	wechatArticleViewList = make([]QuestionView, 0)
+
+	for _, v := range list {
+		wechatArticleViewList = append(wechatArticleViewList, v.ToView())
+	}
+	return
+}
+
+func (m *Question) GetByID(id int) (item *Question, err error) {
+	err = global.DbMap[utils.DbNameAI].Where(fmt.Sprintf("%s = ?", QuestionColumns.QuestionID), id).First(&item).Error
+
+	return
+}
+
+func (m *Question) GetByCondition(condition string, pars []interface{}) (item *Question, err error) {
+	sqlStr := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).First(&item).Error
+
+	return
+}
+
+func (m *Question) GetListByCondition(condition string, pars []interface{}, startSize, pageSize int) (items []*Question, err error) {
+	sqlStr := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s order by question_id desc LIMIT ?,?`, m.TableName(), condition)
+	pars = append(pars, startSize, pageSize)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Find(&items).Error
+
+	return
+}
+
+func (m *Question) GetCountByCondition(condition string, pars []interface{}) (total int, err error) {
+	var intNull sql.NullInt64
+	sqlStr := fmt.Sprintf(`SELECT COUNT(1) total FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Scan(&intNull).Error
+	if err == nil && intNull.Valid {
+		total = int(intNull.Int64)
+	}
+
+	return
+}
+
+func (m *Question) GetPageListByCondition(condition string, pars []interface{}, startSize, pageSize int) (total int, items []*Question, err error) {
+
+	total, err = m.GetCountByCondition(condition, pars)
+	if err != nil {
+		return
+	}
+	if total > 0 {
+		items, err = m.GetListByCondition(condition, pars, startSize, pageSize)
+	}
+
+	return
+}

+ 33 - 0
models/rag/request/wechat_platform.go

@@ -0,0 +1,33 @@
+package request
+
+type AddWechatPlatformReq struct {
+	Name      string `description:"公众号名称"`
+	Link      string `description:"公众号文章链接"`
+	TagIdList []int  `description:"标签列表"`
+}
+
+type OpWechatPlatformReq struct {
+	Status           int `description:"0:禁用,1:启用"`
+	WechatPlatformId int `description:"公众号id"`
+}
+
+type RefreshWechatPlatformReq struct {
+	WechatPlatformId int `description:"公众号id"`
+}
+
+type AddQuestionReq struct {
+	Content string `description:"公众号名称"`
+}
+
+type EditQuestionReq struct {
+	QuestionId int    `description:"问题id"`
+	Content    string `description:"公众号名称"`
+}
+
+type BeachOpAbstractReq struct {
+	WechatArticleAbstractIdList    []int  `description:"摘要id"`
+	NotWechatArticleAbstractIdList []int  `description:"不需要的摘要id"`
+	KeyWord                        string `description:"关键字"`
+	TagId                          int    `description:"标签id"`
+	IsSelectAll                    bool   `description:"是否选择所有摘要"`
+}

+ 11 - 0
models/rag/response/abstract.go

@@ -0,0 +1,11 @@
+package response
+
+import (
+	"eta/eta_api/models/rag"
+	"github.com/rdlucklib/rdluck_tools/paging"
+)
+
+type AbstractListListResp struct {
+	List   []rag.WechatArticleAbstractView
+	Paging *paging.PagingItem `description:"分页数据"`
+}

+ 11 - 0
models/rag/response/question.go

@@ -0,0 +1,11 @@
+package response
+
+import (
+	"eta/eta_api/models/rag"
+	"github.com/rdlucklib/rdluck_tools/paging"
+)
+
+type QuestionListListResp struct {
+	List   []rag.QuestionView
+	Paging *paging.PagingItem `description:"分页数据"`
+}

+ 30 - 0
models/rag/response/wechat_platform.go

@@ -0,0 +1,30 @@
+package response
+
+import (
+	"eta/eta_api/models/rag"
+	"github.com/rdlucklib/rdluck_tools/paging"
+)
+
+type WechatPlatformListResp struct {
+	List   []*rag.WechatPlatform
+	Paging *paging.PagingItem `description:"分页数据"`
+}
+
+type WechatPlatformPublicListResp struct {
+	UserId int    `description:"用户id"`
+	Name   string `description:"研究员名称"`
+	List   []*rag.UserFollowWechatPlatform
+	//Paging *paging.PagingItem `description:"分页数据"`
+}
+
+type TagListResp struct {
+	List   []*rag.Tag
+	Paging *paging.PagingItem `description:"分页数据"`
+}
+
+type WechatArticleListListResp struct {
+	UserId int    `description:"用户id"`
+	Name   string `description:"研究员名称"`
+	List   []rag.WechatArticleView
+	Paging *paging.PagingItem `description:"分页数据"`
+}

+ 83 - 0
models/rag/tag.go

@@ -0,0 +1,83 @@
+package rag
+
+import (
+	"database/sql"
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"time"
+)
+
+// Tag 品种标签
+type Tag struct {
+	TagId      int       `gorm:"column:tag_id;type:int(9) UNSIGNED;primaryKey;not null;" description:"tag_id"`
+	TagName    string    `gorm:"column:tag_name;type:varchar(255);comment:标签名称;" description:"tag_name"`                // 标签名称
+	Sort       int       `gorm:"column:sort;type:int(9);comment:排序字段;default:0;" description:"sort"`                    // 排序字段
+	ModifyTime time.Time `gorm:"column:modify_time;type:datetime;comment:修改时间;default:NULL;" description:"modify_time"` // 修改时间
+	CreateTime time.Time `gorm:"column:create_time;type:datetime;comment:添加时间;default:NULL;" description:"create_time"` // 添加时间
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *Tag) TableName() string {
+	return "tag"
+}
+
+// TagColumns get sql column name.获取数据库列名
+var TagColumns = struct {
+	TagID      string
+	TagName    string
+	Sort       string
+	ModifyTime string
+	CreateTime string
+}{
+	TagID:      "tag_id",
+	TagName:    "tag_name",
+	Sort:       "sort",
+	ModifyTime: "modify_time",
+	CreateTime: "create_time",
+}
+
+func (m *Tag) GetByID(TagId int) (item *Tag, err error) {
+	err = global.DbMap[utils.DbNameAI].Where(fmt.Sprintf("%s = ?", TagColumns.TagID), TagId).First(&item).Error
+
+	return
+}
+
+func (m *Tag) GetByCondition(condition string, pars []interface{}) (item *Tag, err error) {
+	sqlStr := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).First(&item).Error
+
+	return
+}
+
+func (m *Tag) GetListByCondition(condition string, pars []interface{}, startSize, pageSize int) (items []*Tag, err error) {
+	sqlStr := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s LIMIT ?,?`, m.TableName(), condition)
+	pars = append(pars, startSize, pageSize)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Find(&items).Error
+
+	return
+}
+
+func (m *Tag) GetCountByCondition(condition string, pars []interface{}) (total int, err error) {
+	var intNull sql.NullInt64
+	sqlStr := fmt.Sprintf(`SELECT COUNT(1) total FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Scan(&intNull).Error
+	if err == nil && intNull.Valid {
+		total = int(intNull.Int64)
+	}
+
+	return
+}
+
+func (m *Tag) GetPageListByCondition(condition string, pars []interface{}, startSize, pageSize int) (total int, items []*Tag, err error) {
+
+	total, err = m.GetCountByCondition(condition, pars)
+	if err != nil {
+		return
+	}
+	if total > 0 {
+		items, err = m.GetListByCondition(condition, pars, startSize, pageSize)
+	}
+
+	return
+}

+ 302 - 0
models/rag/wechat_article.go

@@ -0,0 +1,302 @@
+package rag
+
+import (
+	"database/sql"
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"time"
+)
+
+type WechatArticle struct {
+	WechatArticleId   int       `gorm:"column:wechat_article_id;type:int(10) UNSIGNED;primaryKey;not null;" description:""`
+	WechatPlatformId  int       `gorm:"column:wechat_platform_id;type:int(11);comment:归属公众号id;default:0;" description:"归属公众号id"`
+	FakeId            string    `gorm:"column:fake_id;type:varchar(255);comment:公众号唯一id;" description:"公众号唯一id"`
+	Title             string    `gorm:"column:title;type:varchar(255);comment:标题;" description:"标题"`
+	Link              string    `gorm:"column:link;type:varchar(255);comment:链接;" description:"链接"`
+	CoverUrl          string    `gorm:"column:cover_url;type:varchar(255);comment:公众号封面;" description:"公众号封面"`
+	Description       string    `gorm:"column:description;type:varchar(255);comment:描述;" description:"描述"`
+	Content           string    `gorm:"column:content;type:longtext;comment:报告详情;" description:"报告详情"`
+	TextContent       string    `gorm:"column:text_content;type:text;comment:文本内容;" description:"文本内容"`
+	AbstractStatus    int       `gorm:"column:abstract_status;type:tinyint(4);comment:摘要生成情况,-1:生成失败,0:待生成,1:已生成;default:0;" description:"摘要生成情况,-1:生成失败,0:待生成,1:已生成"`
+	Country           string    `gorm:"column:country;type:varchar(255);comment:国家;" description:"国家"`
+	Province          string    `gorm:"column:province;type:varchar(255);comment:省;" description:"省"`
+	City              string    `gorm:"column:city;type:varchar(255);comment:市;" description:"市"`
+	ArticleCreateTime time.Time `gorm:"column:article_create_time;type:datetime;comment:报告创建时间;default:NULL;" description:"报告创建时间"`
+	VectorKey         string    `gorm:"column:vector_key;type:varchar(255);comment:向量key标识;" description:"向量key标识"`
+	IsDeleted         int       `gorm:"column:is_deleted;type:tinyint(4);comment:是否删除,0:未删除,1: 已删除;default:0;" description:"是否删除,0:未删除,1: 已删除"`
+	ModifyTime        time.Time `gorm:"column:modify_time;type:datetime;comment:修改时间;default:NULL;" description:"修改时间"`
+	CreateTime        time.Time `gorm:"column:create_time;type:datetime;comment:入库时间;default:NULL;" description:"入库时间"`
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *WechatArticle) TableName() string {
+	return "wechat_article"
+}
+
+// WechatArticleColumns get sql column name.获取数据库列名
+var WechatArticleColumns = struct {
+	WechatArticleID   string
+	WechatPlatformID  string
+	FakeID            string
+	Title             string
+	Link              string
+	CoverURL          string
+	Description       string
+	Content           string
+	TextContent       string
+	AbstractStatus    string
+	Country           string
+	Province          string
+	City              string
+	ArticleCreateTime string
+	IsDeleted         string
+	ModifyTime        string
+	CreateTime        string
+}{
+	WechatArticleID:   "wechat_article_id",
+	WechatPlatformID:  "wechat_platform_id",
+	FakeID:            "fake_id",
+	Title:             "title",
+	Link:              "link",
+	CoverURL:          "cover_url",
+	Description:       "description",
+	Content:           "content",
+	TextContent:       "text_content",
+	AbstractStatus:    "abstract_status",
+	Country:           "country",
+	Province:          "province",
+	City:              "city",
+	ArticleCreateTime: "article_create_time",
+	IsDeleted:         "is_deleted",
+	ModifyTime:        "modify_time",
+	CreateTime:        "create_time",
+}
+
+type WechatArticleView struct {
+	WechatArticleId            int    `gorm:"column:wechat_article_id;type:int(10) UNSIGNED;primaryKey;not null;" description:""`
+	WechatPlatformId           int    `gorm:"column:wechat_platform_id;type:int(11);comment:归属公众号id;default:0;" description:"归属公众号id"`
+	FakeId                     string `gorm:"column:fake_id;type:varchar(255);comment:公众号唯一id;" description:"公众号唯一id"`
+	Title                      string `gorm:"column:title;type:varchar(255);comment:标题;" description:"标题"`
+	Link                       string `gorm:"column:link;type:varchar(255);comment:链接;" description:"链接"`
+	CoverUrl                   string `gorm:"column:cover_url;type:varchar(255);comment:公众号封面;" description:"公众号封面"`
+	Description                string `gorm:"column:description;type:varchar(255);comment:描述;" description:"描述"`
+	Content                    string `gorm:"column:content;type:longtext;comment:报告详情;" description:"报告详情"`
+	TextContent                string `gorm:"column:text_content;type:text;comment:文本内容;" description:"文本内容"`
+	AbstractStatus             int    `gorm:"column:abstract_status;type:tinyint(4);comment:摘要生成情况,-1:生成失败,0:待生成,1:已生成;default:0;" description:"摘要生成情况,-1:生成失败,0:待生成,1:已生成"`
+	Abstract                   string `gorm:"column:abstract;type:text;comment:摘要;" description:"摘要"`
+	Country                    string `gorm:"column:country;type:varchar(255);comment:国家;" description:"国家"`
+	Province                   string `gorm:"column:province;type:varchar(255);comment:省;" description:"省"`
+	City                       string `gorm:"column:city;type:varchar(255);comment:市;" description:"市"`
+	ArticleCreateTime          string `gorm:"column:article_create_time;type:datetime;comment:报告创建时间;default:NULL;" description:"报告创建时间"`
+	ModifyTime                 string `gorm:"column:modify_time;type:datetime;comment:修改时间;default:NULL;" description:"修改时间"`
+	CreateTime                 string `gorm:"column:create_time;type:datetime;comment:入库时间;default:NULL;" description:"入库时间"`
+	WechatPlatformName         string `gorm:"column:title;type:varchar(255);comment:标题;" description:"微信公众号名称"`
+	WechatPlatformRoundHeadImg string `gorm:"column:round_head_img;type:varchar(255);comment:头像;" description:"微信公众号头像"`
+}
+
+func (m *WechatArticle) ToView() WechatArticleView {
+	var articleCreateTime, modifyTime, createTime string
+
+	if !m.ArticleCreateTime.IsZero() {
+		articleCreateTime = m.ArticleCreateTime.Format(utils.FormatDateTime)
+	}
+	if !m.CreateTime.IsZero() {
+		createTime = m.CreateTime.Format(utils.FormatDateTime)
+	}
+	if !m.ModifyTime.IsZero() {
+		modifyTime = m.ModifyTime.Format(utils.FormatDateTime)
+	}
+	return WechatArticleView{
+		WechatArticleId:            m.WechatArticleId,
+		WechatPlatformId:           m.WechatPlatformId,
+		FakeId:                     m.FakeId,
+		Title:                      m.Title,
+		Link:                       m.Link,
+		CoverUrl:                   m.CoverUrl,
+		Description:                m.Description,
+		Content:                    m.Content,
+		TextContent:                m.TextContent,
+		AbstractStatus:             m.AbstractStatus,
+		Country:                    m.Country,
+		Province:                   m.Province,
+		City:                       m.City,
+		ArticleCreateTime:          articleCreateTime,
+		ModifyTime:                 modifyTime,
+		CreateTime:                 createTime,
+		WechatPlatformName:         "",
+		WechatPlatformRoundHeadImg: "",
+	}
+}
+
+func (m *WechatArticle) ListToViewList(list []*WechatArticle) (wechatArticleViewList []WechatArticleView) {
+	wechatArticleViewList = make([]WechatArticleView, 0)
+
+	for _, v := range list {
+		wechatArticleViewList = append(wechatArticleViewList, v.ToView())
+	}
+	return
+}
+
+func (m *WechatArticle) Create() (err error) {
+	err = global.DbMap[utils.DbNameAI].Create(&m).Error
+
+	return
+}
+
+func (m *WechatArticle) Update(updateCols []string) (err error) {
+	err = global.DbMap[utils.DbNameAI].Select(updateCols).Updates(&m).Error
+
+	return
+}
+
+func (m *WechatArticle) GetById(id int) (item *WechatArticle, err error) {
+	err = global.DbMap[utils.DbNameAI].Where(fmt.Sprintf("%s = ?", WechatArticleColumns.WechatArticleID), id).First(&item).Error
+
+	return
+}
+
+func (m *WechatArticle) GetByLink(link string) (item *WechatArticle, err error) {
+	err = global.DbMap[utils.DbNameAI].Where(fmt.Sprintf("%s = ?", WechatArticleColumns.Link), link).First(&item).Error
+
+	return
+}
+
+func (m *WechatArticle) GetListByCondition(field, condition string, pars []interface{}, startSize, pageSize int) (items []*WechatArticle, err error) {
+	if field == "" {
+		field = "*"
+	}
+	sqlStr := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 AND is_deleted=0 %s  order by article_create_time desc,wechat_article_id desc LIMIT ?,?`, field, m.TableName(), condition)
+	pars = append(pars, startSize, pageSize)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Find(&items).Error
+
+	return
+}
+
+func (m *WechatArticle) GetCountByCondition(condition string, pars []interface{}) (total int, err error) {
+	var intNull sql.NullInt64
+	sqlStr := fmt.Sprintf(`SELECT COUNT(1) total FROM %s WHERE 1=1 AND is_deleted=0 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Scan(&intNull).Error
+	if err == nil && intNull.Valid {
+		total = int(intNull.Int64)
+	}
+
+	return
+}
+
+func (m *WechatArticle) GetPageListByCondition(condition string, pars []interface{}, startSize, pageSize int) (total int, items []*WechatArticle, err error) {
+
+	total, err = m.GetCountByCondition(condition, pars)
+	if err != nil {
+		return
+	}
+	if total > 0 {
+		items, err = m.GetListByCondition(`wechat_article_id,wechat_platform_id,fake_id,title,link,cover_url,description,country,province,city,article_create_time,modify_time,create_time`, condition, pars, startSize, pageSize)
+	}
+
+	return
+}
+
+type WechatArticleAndPlatform struct {
+	WechatArticleId   int       `gorm:"column:wechat_article_id;type:int(10) UNSIGNED;primaryKey;not null;" description:""`
+	WechatPlatformId  int       `gorm:"column:wechat_platform_id;type:int(11);comment:归属公众号id;default:0;" description:"归属公众号id"`
+	FakeId            string    `gorm:"column:fake_id;type:varchar(255);comment:公众号唯一id;" description:"公众号唯一id"`
+	Title             string    `gorm:"column:title;type:varchar(255);comment:标题;" description:"标题"`
+	Link              string    `gorm:"column:link;type:varchar(255);comment:链接;" description:"链接"`
+	CoverUrl          string    `gorm:"column:cover_url;type:varchar(255);comment:公众号封面;" description:"公众号封面"`
+	Description       string    `gorm:"column:description;type:varchar(255);comment:描述;" description:"描述"`
+	Content           string    `gorm:"column:content;type:longtext;comment:报告详情;" description:"报告详情"`
+	TextContent       string    `gorm:"column:text_content;type:text;comment:文本内容;" description:"文本内容"`
+	Abstract          string    `gorm:"column:abstract;type:text;comment:摘要;" description:"摘要"`
+	Country           string    `gorm:"column:country;type:varchar(255);comment:国家;" description:"国家"`
+	Province          string    `gorm:"column:province;type:varchar(255);comment:省;" description:"省"`
+	City              string    `gorm:"column:city;type:varchar(255);comment:市;" description:"市"`
+	ArticleCreateTime time.Time `gorm:"column:article_create_time;type:datetime;comment:报告创建时间;default:NULL;" description:"报告创建时间"`
+	IsDeleted         int       `gorm:"column:is_deleted;type:tinyint(4);comment:是否删除,0:未删除,1: 已删除;default:0;" description:"是否删除,0:未删除,1: 已删除"`
+	ModifyTime        time.Time `gorm:"column:modify_time;type:datetime;comment:修改时间;default:NULL;" description:"修改时间"`
+	CreateTime        time.Time `gorm:"column:create_time;type:datetime;comment:入库时间;default:NULL;" description:"入库时间"`
+	Nickname          string    `gorm:"column:nickname;type:varchar(255);comment:公众号名称;" description:"nickname"`          // 公众号名称
+	Alias             string    `gorm:"column:alias;type:varchar(255);comment:别名;" description:"alias"`                   // 别名
+	RoundHeadImg      string    `gorm:"column:round_head_img;type:varchar(255);comment:头像;" description:"round_head_img"` // 头像
+}
+
+func (m *WechatArticleAndPlatform) ToView() WechatArticleView {
+	var articleCreateTime, modifyTime, createTime string
+
+	if !m.ArticleCreateTime.IsZero() {
+		articleCreateTime = m.ArticleCreateTime.Format(utils.FormatDateTime)
+	}
+	if !m.CreateTime.IsZero() {
+		createTime = m.CreateTime.Format(utils.FormatDateTime)
+	}
+	if !m.ModifyTime.IsZero() {
+		modifyTime = m.ModifyTime.Format(utils.FormatDateTime)
+	}
+	return WechatArticleView{
+		WechatArticleId:            m.WechatArticleId,
+		WechatPlatformId:           m.WechatPlatformId,
+		FakeId:                     m.FakeId,
+		Title:                      m.Title,
+		Link:                       m.Link,
+		CoverUrl:                   m.CoverUrl,
+		Description:                m.Description,
+		Content:                    m.Content,
+		TextContent:                m.TextContent,
+		Abstract:                   m.Abstract,
+		Country:                    m.Country,
+		Province:                   m.Province,
+		City:                       m.City,
+		ArticleCreateTime:          articleCreateTime,
+		ModifyTime:                 modifyTime,
+		CreateTime:                 createTime,
+		WechatPlatformName:         m.Nickname,
+		WechatPlatformRoundHeadImg: m.RoundHeadImg,
+	}
+}
+
+func (m *WechatArticle) ArticleAndPlatformListToViewList(list []*WechatArticleAndPlatform) (wechatArticleViewList []WechatArticleView) {
+	wechatArticleViewList = make([]WechatArticleView, 0)
+
+	for _, v := range list {
+		wechatArticleViewList = append(wechatArticleViewList, v.ToView())
+	}
+	return
+}
+
+func (m *WechatArticle) GetListByPlatformCondition(field, condition string, pars []interface{}, startSize, pageSize int) (items []*WechatArticleAndPlatform, err error) {
+	if field == "" {
+		field = "*"
+	}
+	sqlStr := fmt.Sprintf(`SELECT %s FROM %s AS a 
+          JOIN wechat_platform AS b ON a.wechat_platform_id=b.wechat_platform_id
+          WHERE 1=1 AND a.is_deleted=0 %s  order by a.article_create_time DESC,a.wechat_article_id DESC LIMIT ?,?`, field, m.TableName(), condition)
+	pars = append(pars, startSize, pageSize)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Find(&items).Error
+
+	return
+}
+
+func (m *WechatArticle) GetCountByPlatformCondition(condition string, pars []interface{}) (total int, err error) {
+	var intNull sql.NullInt64
+	sqlStr := fmt.Sprintf(`SELECT COUNT(1) total FROM %s AS a 
+          JOIN wechat_platform AS b ON a.wechat_platform_id=b.wechat_platform_id 
+          WHERE 1=1 AND a.is_deleted=0 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Scan(&intNull).Error
+	if err == nil && intNull.Valid {
+		total = int(intNull.Int64)
+	}
+
+	return
+}
+
+func (m *WechatArticle) GetPageListByPlatformCondition(condition string, pars []interface{}, startSize, pageSize int) (total int, items []*WechatArticleAndPlatform, err error) {
+	total, err = m.GetCountByPlatformCondition(condition, pars)
+	if err != nil {
+		return
+	}
+	if total > 0 {
+		items, err = m.GetListByPlatformCondition(`a.wechat_article_id,a.wechat_platform_id,a.fake_id,a.title,a.link,a.cover_url,a.description,a.country,a.province,a.city,a.article_create_time,a.modify_time,a.create_time,b.nickname,b.round_head_img`, condition, pars, startSize, pageSize)
+	}
+
+	return
+}

+ 255 - 0
models/rag/wechat_article_abstract.go

@@ -0,0 +1,255 @@
+package rag
+
+import (
+	"database/sql"
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"time"
+)
+
+type WechatArticleAbstract struct {
+	WechatArticleAbstractId int       `gorm:"column:wechat_article_abstract_id;type:int(9) UNSIGNED;primaryKey;not null;" description:"wechat_article_abstract_id"`
+	WechatArticleId         int       `gorm:"column:wechat_article_id;type:int(9) UNSIGNED;comment:关联的微信报告id;default:0;" description:"关联的微信报告id"`
+	Content                 string    `gorm:"column:content;type:longtext;comment:摘要内容;" description:"content"` // 摘要内容
+	Version                 int       `gorm:"column:version;type:int(10) UNSIGNED;comment:版本号;default:1;" description:"版本号"`
+	VectorKey               string    `gorm:"column:vector_key;type:varchar(255);comment:向量key标识;" description:"向量key标识"`
+	ModifyTime              time.Time `gorm:"column:modify_time;type:datetime;default:NULL;" description:"modify_time"`
+	CreateTime              time.Time `gorm:"column:create_time;type:datetime;default:NULL;" description:"create_time"`
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *WechatArticleAbstract) TableName() string {
+	return "wechat_article_abstract"
+}
+
+// WechatArticleAbstractColumns get sql column name.获取数据库列名
+var WechatArticleAbstractColumns = struct {
+	WechatArticleAbstractID string
+	WechatArticleID         string
+	Content                 string
+	Version                 string
+	ModifyTime              string
+	CreateTime              string
+}{
+	WechatArticleAbstractID: "wechat_article_abstract_id",
+	WechatArticleID:         "wechat_article_id",
+	Content:                 "content",
+	Version:                 "version",
+	ModifyTime:              "modify_time",
+	CreateTime:              "create_time",
+}
+
+func (m *WechatArticleAbstract) Create() (err error) {
+	err = global.DbMap[utils.DbNameAI].Create(&m).Error
+
+	return
+}
+
+func (m *WechatArticleAbstract) Update(updateCols []string) (err error) {
+	err = global.DbMap[utils.DbNameAI].Select(updateCols).Updates(&m).Error
+
+	return
+}
+
+func (m *WechatArticleAbstract) Del() (err error) {
+	err = global.DbMap[utils.DbNameAI].Delete(&m).Error
+
+	return
+}
+
+func (m *WechatArticleAbstract) GetById(id int) (item *WechatArticleAbstract, err error) {
+	err = global.DbMap[utils.DbNameAI].Where(fmt.Sprintf("%s = ?", WechatArticleAbstractColumns.WechatArticleAbstractID), id).First(&item).Error
+
+	return
+}
+
+func (m *WechatArticleAbstract) GetByIdList(idList []int) (items []*WechatArticleAbstract, err error) {
+	err = global.DbMap[utils.DbNameAI].Where(fmt.Sprintf("%s in (?) ", WechatArticleAbstractColumns.WechatArticleAbstractID), idList).Find(&items).Error
+
+	return
+}
+
+func (m *WechatArticleAbstract) GetListByCondition(field, condition string, pars []interface{}, startSize, pageSize int) (items []*WechatArticleAbstract, err error) {
+	if field == "" {
+		field = "*"
+	}
+	sqlStr := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s  order by wechat_article_abstract_id desc LIMIT ?,?`, field, m.TableName(), condition)
+	pars = append(pars, startSize, pageSize)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Find(&items).Error
+
+	return
+}
+
+func (m *WechatArticleAbstract) DelByIdList(idList []int) (err error) {
+	if len(idList) <= 0 {
+		return
+	}
+	sqlStr := fmt.Sprintf(`delete from %s where %s in (?)`, m.TableName(), WechatArticleAbstractColumns.WechatArticleAbstractID)
+	err = global.DbMap[utils.DbNameAI].Exec(sqlStr, idList).Error
+
+	return
+}
+
+// GetByWechatArticleId
+// @Description: 根据报告id获取摘要
+// @author: Roc
+// @receiver m
+// @datetime 2025-03-07 10:00:59
+// @param id int
+// @return item *WechatArticleAbstract
+// @return err error
+func (m *WechatArticleAbstract) GetByWechatArticleId(id int) (item *WechatArticleAbstract, err error) {
+	err = global.DbMap[utils.DbNameAI].Where(fmt.Sprintf("%s = ?", WechatArticleAbstractColumns.WechatArticleID), id).Order(fmt.Sprintf(`%s DESC`, WechatArticleAbstractColumns.WechatArticleAbstractID)).First(&item).Error
+
+	return
+}
+
+type WechatArticleAbstractView struct {
+	WechatArticleAbstractId int    `gorm:"column:wechat_article_abstract_id;type:int(9) UNSIGNED;primaryKey;not null;" description:"wechat_article_abstract_id"`
+	WechatArticleId         int    `gorm:"column:wechat_article_id;type:int(9) UNSIGNED;comment:关联的微信报告id;default:0;" description:"关联的微信报告id"`
+	WechatPlatformId        int    `gorm:"column:wechat_platform_id;type:int(11);comment:归属公众号id;default:0;" description:"归属公众号id"`
+	Abstract                string `gorm:"column:abstract;type:longtext;comment:摘要内容;" description:"摘要内容"` //
+	Version                 int    `gorm:"column:version;type:int(10) UNSIGNED;comment:版本号;default:1;" description:"版本号"`
+	VectorKey               string `gorm:"column:vector_key;type:varchar(255);comment:向量key标识;" description:"向量key标识"`
+	ModifyTime              string `gorm:"column:modify_time;type:datetime;default:NULL;" description:"modify_time"`
+	CreateTime              string `gorm:"column:create_time;type:datetime;default:NULL;" description:"create_time"`
+	Title                   string `gorm:"column:title;type:varchar(255);comment:标题;" description:"标题"`
+	Link                    string `gorm:"column:link;type:varchar(255);comment:链接;" description:"链接"`
+	TagId                   int    `gorm:"column:tag_id;type:int(9) UNSIGNED;comment:品种id;default:0;" description:"品种id"`
+}
+
+type WechatArticleAbstractItem struct {
+	WechatArticleAbstractId int       `gorm:"column:wechat_article_abstract_id;type:int(9) UNSIGNED;primaryKey;not null;" description:"wechat_article_abstract_id"`
+	WechatArticleId         int       `gorm:"column:wechat_article_id;type:int(9) UNSIGNED;comment:关联的微信报告id;default:0;" description:"关联的微信报告id"`
+	Abstract                string    `gorm:"column:abstract;type:longtext;comment:摘要内容;" description:"摘要内容"` //
+	Version                 int       `gorm:"column:version;type:int(10) UNSIGNED;comment:版本号;default:1;" description:"版本号"`
+	VectorKey               string    `gorm:"column:vector_key;type:varchar(255);comment:向量key标识;" description:"向量key标识"`
+	ModifyTime              time.Time `gorm:"column:modify_time;type:datetime;default:NULL;" description:"modify_time"`
+	CreateTime              time.Time `gorm:"column:create_time;type:datetime;default:NULL;" description:"create_time"`
+	Title                   string    `gorm:"column:title;type:varchar(255);comment:标题;" description:"标题"`
+	Link                    string    `gorm:"column:link;type:varchar(255);comment:链接;" description:"链接"`
+	TagId                   int       `gorm:"column:tag_id;type:int(9) UNSIGNED;comment:品种id;default:0;" description:"品种id"`
+}
+
+func (m *WechatArticleAbstractItem) ToView() WechatArticleAbstractView {
+	return WechatArticleAbstractView{
+		WechatArticleAbstractId: m.WechatArticleAbstractId,
+		WechatArticleId:         m.WechatArticleId,
+		Abstract:                m.Abstract,
+		Version:                 m.Version,
+		VectorKey:               m.VectorKey,
+		ModifyTime:              utils.DateStrToDateTimeStr(m.ModifyTime),
+		CreateTime:              utils.DateStrToDateTimeStr(m.CreateTime),
+		Title:                   m.Title,
+		Link:                    m.Link,
+		TagId:                   m.TagId,
+	}
+}
+
+func (m *WechatArticleAbstract) WechatArticleAbstractItem(list []*WechatArticleAbstractItem) (wechatArticleViewList []WechatArticleAbstractView) {
+	wechatArticleViewList = make([]WechatArticleAbstractView, 0)
+
+	for _, v := range list {
+		wechatArticleViewList = append(wechatArticleViewList, v.ToView())
+	}
+	return
+}
+
+func (m *WechatArticleAbstract) GetListByPlatformCondition(field, condition string, pars []interface{}, startSize, pageSize int) (items []*WechatArticleAbstractItem, err error) {
+	if field == "" {
+		field = "*"
+	}
+	sqlStr := fmt.Sprintf(`SELECT %s FROM %s AS a 
+          JOIN wechat_article AS b ON a.wechat_article_id=b.wechat_article_id
+          JOIN wechat_platform AS c ON b.wechat_platform_id=c.wechat_platform_id
+          WHERE 1=1 AND b.is_deleted=0 %s  order by a.modify_time DESC,a.wechat_article_abstract_id DESC LIMIT ?,?`, field, m.TableName(), condition)
+	pars = append(pars, startSize, pageSize)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Find(&items).Error
+
+	return
+}
+
+func (m *WechatArticleAbstract) GetCountByPlatformCondition(condition string, pars []interface{}) (total int, err error) {
+	var intNull sql.NullInt64
+	sqlStr := fmt.Sprintf(`SELECT COUNT(1) total FROM %s AS a 
+          JOIN wechat_article AS b ON a.wechat_article_id=b.wechat_article_id
+          JOIN wechat_platform AS c ON b.wechat_platform_id=c.wechat_platform_id
+          WHERE 1=1 AND b.is_deleted=0 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Scan(&intNull).Error
+	if err == nil && intNull.Valid {
+		total = int(intNull.Int64)
+	}
+
+	return
+}
+
+func (m *WechatArticleAbstract) GetPageListByPlatformCondition(condition string, pars []interface{}, startSize, pageSize int) (total int, items []*WechatArticleAbstractItem, err error) {
+
+	total, err = m.GetCountByPlatformCondition(condition, pars)
+	if err != nil {
+		return
+	}
+	if total > 0 {
+		items, err = m.GetListByPlatformCondition(`a.wechat_article_abstract_id,a.wechat_article_id,a.content AS abstract,a.version,a.vector_key,b.title,b.link,a.modify_time,a.create_time`, condition, pars, startSize, pageSize)
+	}
+
+	return
+}
+
+func (m *WechatArticleAbstract) GetListByTagAndPlatformCondition(field, condition string, pars []interface{}, startSize, pageSize int) (items []*WechatArticleAbstractItem, err error) {
+	if field == "" {
+		field = "*"
+	}
+	sqlStr := fmt.Sprintf(`SELECT %s FROM %s AS a 
+          JOIN wechat_article AS b ON a.wechat_article_id=b.wechat_article_id
+          JOIN wechat_platform AS c ON b.wechat_platform_id=c.wechat_platform_id
+          JOIN wechat_platform_tag_mapping AS d ON c.wechat_platform_id=d.wechat_platform_id
+          WHERE 1=1 AND b.is_deleted=0 %s  order by a.modify_time DESC,a.wechat_article_abstract_id DESC LIMIT ?,?`, field, m.TableName(), condition)
+	pars = append(pars, startSize, pageSize)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Find(&items).Error
+
+	return
+}
+
+func (m *WechatArticleAbstract) GetCountByTagAndPlatformCondition(condition string, pars []interface{}) (total int, err error) {
+	var intNull sql.NullInt64
+	sqlStr := fmt.Sprintf(`SELECT COUNT(1) total FROM %s AS a 
+          JOIN wechat_article AS b ON a.wechat_article_id=b.wechat_article_id
+          JOIN wechat_platform AS c ON b.wechat_platform_id=c.wechat_platform_id
+          JOIN wechat_platform_tag_mapping AS d ON c.wechat_platform_id=d.wechat_platform_id
+          WHERE 1=1 AND b.is_deleted=0 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Scan(&intNull).Error
+	if err == nil && intNull.Valid {
+		total = int(intNull.Int64)
+	}
+
+	return
+}
+
+func (m *WechatArticleAbstract) GetPageListByTagAndPlatformCondition(condition string, pars []interface{}, startSize, pageSize int) (total int, items []*WechatArticleAbstractItem, err error) {
+
+	total, err = m.GetCountByTagAndPlatformCondition(condition, pars)
+	if err != nil {
+		return
+	}
+	if total > 0 {
+		items, err = m.GetListByTagAndPlatformCondition(`a.wechat_article_abstract_id,a.wechat_article_id,a.content AS abstract,a.version,a.vector_key,a.modify_time,a.create_time,b.title,b.link,d.tag_id`, condition, pars, startSize, pageSize)
+	}
+
+	return
+}
+
+// DelVectorKey
+// @Description: 批量删除向量库
+// @author: Roc
+// @receiver m
+// @datetime 2025-03-12 16:47:52
+// @param wechatArticleAbstractIdList []int
+// @return err error
+func (m *WechatArticleAbstract) DelVectorKey(wechatArticleAbstractIdList []int) (err error) {
+	sqlStr := fmt.Sprintf(`UPDATE %s set vector_key = '' WHERE wechat_article_abstract_id IN (?)`, m.TableName())
+	err = global.DbMap[utils.DbNameAI].Exec(sqlStr, wechatArticleAbstractIdList).Error
+
+	return
+}

+ 66 - 0
models/rag/wechat_article_chat_record.go

@@ -0,0 +1,66 @@
+package rag
+
+import (
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"time"
+)
+
+type WechatArticleChatRecord struct {
+	WechatArticleChatRecordId int       `gorm:"column:wechat_article_chat_record_id;type:int(11);comment:主键;primaryKey;not null;" json:"wechat_article_chat_record_id"` // 主键
+	WechatArticleId           int       `gorm:"column:wechat_article_id;type:int(11);comment:文章id;default:NULL;" json:"wechat_article_id"`                              // 文章id
+	ChatUserType              string    `gorm:"column:chat_user_type;type:enum('user', 'assistant');comment:用户方;default:NULL;" json:"chat_user_type"`                   // 用户方
+	Content                   string    `gorm:"column:content;type:longtext;comment:对话内容;" json:"content"`                                                              // 对话内容
+	SendTime                  time.Time `gorm:"column:send_time;type:datetime;comment:发送时间;default:NULL;" json:"send_time"`                                             // 发送时间
+	CreatedTime               time.Time `gorm:"column:created_time;type:datetime;comment:创建时间;default:NULL;" json:"created_time"`                                       // 创建时间
+	UpdateTime                time.Time `gorm:"column:update_time;type:datetime;comment:更新时间;default:NULL;" json:"update_time"`                                         // 更新时间
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *WechatArticleChatRecord) TableName() string {
+	return "wechat_article_chat_record"
+}
+
+// WechatArticleChatRecordColumns get sql column name.获取数据库列名
+var WechatArticleChatRecordColumns = struct {
+	WechatArticleChatRecordID string
+	WechatArticleID           string
+	ChatUserType              string
+	Content                   string
+	SendTime                  string
+	CreatedTime               string
+	UpdateTime                string
+}{
+	WechatArticleChatRecordID: "wechat_article_chat_record_id",
+	WechatArticleID:           "wechat_article_id",
+	ChatUserType:              "chat_user_type",
+	Content:                   "content",
+	SendTime:                  "send_time",
+	CreatedTime:               "created_time",
+	UpdateTime:                "update_time",
+}
+
+func (m *WechatArticleChatRecord) Create() (err error) {
+	err = global.DbMap[utils.DbNameAI].Create(&m).Error
+
+	return
+}
+
+func (m *WechatArticleChatRecord) CreateInBatches(items []*WechatArticleChatRecord) (err error) {
+	err = global.DbMap[utils.DbNameAI].CreateInBatches(items, utils.MultiAddNum).Error
+
+	return
+}
+
+func (m *WechatArticleChatRecord) Update(updateCols []string) (err error) {
+	err = global.DbMap[utils.DbNameAI].Select(updateCols).Updates(&m).Error
+
+	return
+}
+
+func (m *WechatArticleChatRecord) GetById(id int) (item *WechatArticle, err error) {
+	err = global.DbMap[utils.DbNameAI].Where(fmt.Sprintf("%s = ?", WechatArticleChatRecordColumns.WechatArticleChatRecordID), id).First(&item).Error
+
+	return
+}

+ 184 - 0
models/rag/wechat_platform.go

@@ -0,0 +1,184 @@
+package rag
+
+import (
+	"database/sql"
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"time"
+)
+
+type WechatPlatform struct {
+	WechatPlatformId int       `gorm:"column:wechat_platform_id;type:int(10) UNSIGNED;primaryKey;not null;" description:"wechat_platform_id"`
+	FakeId           string    `gorm:"column:fake_id;type:varchar(255);comment:公众号唯一id;" description:"fake_id"`                             // 公众号唯一id
+	Nickname         string    `gorm:"column:nickname;type:varchar(255);comment:公众号名称;" description:"nickname"`                             // 公众号名称
+	Alias            string    `gorm:"column:alias;type:varchar(255);comment:别名;" description:"alias"`                                      // 别名
+	RoundHeadImg     string    `gorm:"column:round_head_img;type:varchar(255);comment:头像;" description:"round_head_img"`                    // 头像
+	ServiceType      int       `gorm:"column:service_type;type:int(11);comment:类型;default:0;" description:"service_type"`                   // 类型
+	Signature        string    `gorm:"column:signature;type:varchar(255);comment:签名;" description:"signature"`                              // 签名
+	Verified         int       `gorm:"column:verified;type:int(11);comment:是否认证,0:未认证,1:已认证;这个我不确定,再核实下;default:0;" description:"verified"` // 是否认证,0:未认证,1:已认证;这个我不确定,再核实下
+	ArticleLink      string    `gorm:"column:article_link;type:varchar(255);comment:添加公众时的文章链接;" description:"article_link"`                // 添加公众时的文章链接
+	Enabled          int       `gorm:"column:enabled;type:tinyint(9);comment:是否启用,0:禁用,1:启用;default:1;" description:"enabled"`              // 是否启用,0:禁用,1:启用
+	SysUserId        int       `gorm:"column:sys_user_id;type:int(9) UNSIGNED;comment:用户id;default:0;" description:"sys_user_id"`           // 用户id
+	ModifyTime       time.Time `gorm:"column:modify_time;type:datetime;comment:最后一次修改时间;default:NULL;" description:"modify_time"`           // 最后一次修改时间
+	CreateTime       time.Time `gorm:"column:create_time;type:datetime;comment:添加时间;default:NULL;" description:"create_time"`               // 添加时间
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *WechatPlatform) TableName() string {
+	return "wechat_platform"
+}
+
+// WechatPlatformColumns get sql column name.获取数据库列名
+var WechatPlatformColumns = struct {
+	WechatPlatformID string
+	FakeID           string
+	Nickname         string
+	Alias            string
+	RoundHeadImg     string
+	ServiceType      string
+	Signature        string
+	Verified         string
+	ArticleLink      string
+	Enabled          string
+	SysUserID        string
+	ModifyTime       string
+	CreateTime       string
+}{
+	WechatPlatformID: "wechat_platform_id",
+	FakeID:           "fake_id",
+	Nickname:         "nickname",
+	Alias:            "alias",
+	RoundHeadImg:     "round_head_img",
+	ServiceType:      "service_type",
+	Signature:        "signature",
+	Verified:         "verified",
+	ArticleLink:      "article_link",
+	Enabled:          "enabled",
+	SysUserID:        "sys_user_id",
+	ModifyTime:       "modify_time",
+	CreateTime:       "create_time",
+}
+
+func (m *WechatPlatform) Create() (err error) {
+	err = global.DbMap[utils.DbNameAI].Create(&m).Error
+
+	return
+}
+
+func (m *WechatPlatform) Update(updateCols []string) (err error) {
+	err = global.DbMap[utils.DbNameAI].Select(updateCols).Updates(&m).Error
+
+	return
+}
+
+func (m *WechatPlatform) Del() (err error) {
+	err = global.DbMap[utils.DbNameAI].Delete(&m).Error
+
+	return
+}
+
+func (m *WechatPlatform) GetById(wechatPlatformId int) (item *WechatPlatform, err error) {
+	err = global.DbMap[utils.DbNameAI].Where(fmt.Sprintf("%s = ?", WechatPlatformColumns.WechatPlatformID), wechatPlatformId).First(&item).Error
+
+	return
+}
+
+func (m *WechatPlatform) GetByCondition(condition string, pars []interface{}) (item *WechatPlatform, err error) {
+	sqlStr := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).First(&item).Error
+
+	return
+}
+
+func (m *WechatPlatform) GetListByCondition(condition string, pars []interface{}, startSize, pageSize int) (items []*WechatPlatform, err error) {
+	sqlStr := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s LIMIT ?,?`, m.TableName(), condition)
+	pars = append(pars, startSize, pageSize)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Find(&items).Error
+
+	return
+}
+
+func (m *WechatPlatform) GetCountByCondition(condition string, pars []interface{}) (total int, err error) {
+	var intNull sql.NullInt64
+	sqlStr := fmt.Sprintf(`SELECT COUNT(1) total FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Scan(&intNull).Error
+	if err == nil && intNull.Valid {
+		total = int(intNull.Int64)
+	}
+
+	return
+}
+
+func (m *WechatPlatform) GetPageListByCondition(condition string, pars []interface{}, startSize, pageSize int) (total int, items []*WechatPlatform, err error) {
+
+	total, err = m.GetCountByCondition(condition, pars)
+	if err != nil {
+		return
+	}
+	if total > 0 {
+		items, err = m.GetListByCondition(condition, pars, startSize, pageSize)
+	}
+
+	return
+}
+
+func (m *WechatPlatform) GetByFakeID(fakeId string) (item *WechatPlatform, err error) {
+	err = global.DbMap[utils.DbNameAI].Where(fmt.Sprintf("%s = ?", WechatPlatformColumns.FakeID), fakeId).First(&item).Error
+
+	return
+}
+
+// Add
+// @Description: 添加一个新的公众号
+// @author: Roc
+// @receiver m
+// @datetime 2025-03-04 17:48:30
+// @param tagIdList []int
+// @return err error
+func (m *WechatPlatform) Add(tagIdList []int) (err error) {
+	tx := global.DbMap[utils.DbNameAI].Begin()
+	defer func() {
+		if err != nil {
+			_ = tx.Rollback()
+		} else {
+			_ = tx.Commit()
+		}
+	}()
+
+	err = tx.Create(&m).Error
+	if err != nil {
+		return
+	}
+
+	// 标签与公众号关系
+	if len(tagIdList) > 0 {
+		addTagMappingList := make([]*WechatPlatformTagMapping, 0)
+
+		for _, tagId := range tagIdList {
+			addTagMappingList = append(addTagMappingList, &WechatPlatformTagMapping{
+				WechatPlatformTagMappingId: 0,
+				WechatPlatformId:           m.WechatPlatformId,
+				TagId:                      tagId,
+				ModifyTime:                 time.Now(),
+				CreateTime:                 time.Now(),
+			})
+		}
+		err = tx.CreateInBatches(addTagMappingList, utils.MultiAddNum).Error
+		if err != nil {
+			return
+		}
+	}
+
+	// 用户与公众号关系
+	userMapping := &WechatPlatformUserMapping{
+		WechatPlatformUserMappingId: 0,
+		WechatPlatformId:            m.WechatPlatformId,
+		SysUserId:                   m.SysUserId,
+		ModifyTime:                  time.Now(),
+		CreateTime:                  time.Now(),
+	}
+	err = tx.Create(userMapping).Error
+
+	return
+}

+ 82 - 0
models/rag/wechat_platform_tag_mapping.go

@@ -0,0 +1,82 @@
+package rag
+
+import (
+	"database/sql"
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"time"
+)
+
+type WechatPlatformTagMapping struct {
+	WechatPlatformTagMappingId int       `gorm:"column:wechat_platform_tag_mapping_id;type:int(9) UNSIGNED;primaryKey;not null;" description:"wechat_platform_tag_mapping_id"`
+	WechatPlatformId           int       `gorm:"column:wechat_platform_id;type:int(9) UNSIGNED;comment:微信公众号id;default:0;" description:"wechat_platform_id"` // 微信公众号id
+	TagId                      int       `gorm:"column:tag_id;type:int(9) UNSIGNED;comment:品种id;default:0;" description:"tag_id"`                            // 品种id
+	ModifyTime                 time.Time `gorm:"column:modify_time;type:datetime;default:NULL;" description:"modify_time"`
+	CreateTime                 time.Time `gorm:"column:create_time;type:datetime;default:NULL;" description:"create_time"`
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *WechatPlatformTagMapping) TableName() string {
+	return "wechat_platform_tag_mapping"
+}
+
+// WechatPlatformTagMappingColumns get sql column name.获取数据库列名
+var WechatPlatformTagMappingColumns = struct {
+	WechatPlatformTagMappingID string
+	WechatPlatformID           string
+	TagID                      string
+	ModifyTime                 string
+	CreateTime                 string
+}{
+	WechatPlatformTagMappingID: "wechat_platform_tag_mapping_id",
+	WechatPlatformID:           "wechat_platform_id",
+	TagID:                      "tag_id",
+	ModifyTime:                 "modify_time",
+	CreateTime:                 "create_time",
+}
+
+func (m *WechatPlatformTagMapping) GetByID(WechatPlatformTagMappingColumnsId int) (item *WechatPlatformTagMapping, err error) {
+	err = global.DbMap[utils.DbNameAI].Where(fmt.Sprintf("%s = ?", WechatPlatformTagMappingColumns.WechatPlatformTagMappingID), WechatPlatformTagMappingColumnsId).First(&item).Error
+
+	return
+}
+
+func (m *WechatPlatformTagMapping) GetByCondition(condition string, pars []interface{}) (item *WechatPlatformTagMapping, err error) {
+	sqlStr := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Where(condition, pars).First(&item).Error
+
+	return
+}
+
+func (m *WechatPlatformTagMapping) GetListByCondition(condition string, pars []interface{}, startSize, pageSize int) (items []*WechatPlatformTagMapping, err error) {
+	sqlStr := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s LIMIT ?,?`, m.TableName(), condition)
+	pars = append(pars, startSize, pageSize)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Find(&items).Error
+
+	return
+}
+
+func (m *WechatPlatformTagMapping) GetCountByCondition(condition string, pars []interface{}) (total int, err error) {
+	var intNull sql.NullInt64
+	sqlStr := fmt.Sprintf(`SELECT COUNT(1) total FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Scan(&intNull).Error
+	if err == nil && intNull.Valid {
+		total = int(intNull.Int64)
+	}
+
+	return
+}
+
+func (m *WechatPlatformTagMapping) GetPageListByCondition(condition string, pars []interface{}, startSize, pageSize int) (total int, items []*WechatPlatformTagMapping, err error) {
+
+	total, err = m.GetCountByCondition(condition, pars)
+	if err != nil {
+		return
+	}
+	if total > 0 {
+		items, err = m.GetListByCondition(condition, pars, startSize, pageSize)
+	}
+
+	return
+}

+ 88 - 0
models/rag/wechat_platform_user_mapping.go

@@ -0,0 +1,88 @@
+package rag
+
+import (
+	"database/sql"
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"time"
+)
+
+type WechatPlatformUserMapping struct {
+	WechatPlatformUserMappingId int       `gorm:"column:wechat_platform_user_mapping_id;type:int(9) UNSIGNED;primaryKey;not null;" description:"wechat_platform_user_mapping_id"`
+	WechatPlatformId            int       `gorm:"column:wechat_platform_id;type:int(9) UNSIGNED;comment:微信公众号id;default:0;" description:"wechat_platform_id"` // 微信公众号id
+	SysUserId                   int       `gorm:"column:sys_user_id;type:int(9) UNSIGNED;comment:用户id;default:0;" description:"sys_user_id"`                  // 用户id
+	ModifyTime                  time.Time `gorm:"column:modify_time;type:datetime;default:NULL;" description:"modify_time"`
+	CreateTime                  time.Time `gorm:"column:create_time;type:datetime;default:NULL;" description:"create_time"`
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *WechatPlatformUserMapping) TableName() string {
+	return "wechat_platform_user_mapping"
+}
+
+// WechatPlatformUserMappingColumns get sql column name.获取数据库列名
+var WechatPlatformUserMappingColumns = struct {
+	WechatPlatformUserMappingID string
+	WechatPlatformID            string
+	SysUserID                   string
+	ModifyTime                  string
+	CreateTime                  string
+}{
+	WechatPlatformUserMappingID: "wechat_platform_user_mapping_id",
+	WechatPlatformID:            "wechat_platform_id",
+	SysUserID:                   "sys_user_id",
+	ModifyTime:                  "modify_time",
+	CreateTime:                  "create_time",
+}
+
+type UserFollowWechatPlatform struct {
+	WechatPlatformId int       `gorm:"column:wechat_platform_id;type:int(10) UNSIGNED;primaryKey;not null;" description:"wechat_platform_id"`
+	FakeId           string    `gorm:"column:fake_id;type:varchar(255);comment:公众号唯一id;" description:"公众号唯一id"`
+	Nickname         string    `gorm:"column:nickname;type:varchar(255);comment:公众号名称;" description:"公众号名称"`
+	Alias            string    `gorm:"column:alias;type:varchar(255);comment:别名;" description:"别名"`
+	RoundHeadImg     string    `gorm:"column:round_head_img;type:varchar(255);comment:头像;" description:"头像"`
+	ServiceType      int       `gorm:"column:service_type;type:int(11);comment:类型;default:0;" description:"类型"`
+	Signature        string    `gorm:"column:signature;type:varchar(255);comment:签名;" description:"签名"`
+	Verified         int       `gorm:"column:verified;type:int(11);comment:是否认证,0:未认证,1:已认证;这个我不确定,再核实下;default:0;" description:"是否认证,0:未认证,1:已认证;这个我不确定,再核实下"`
+	ArticleLink      string    `gorm:"column:article_link;type:varchar(255);comment:添加公众时的文章链接;" description:"添加公众时的文章链接"`
+	Enabled          int       `gorm:"column:enabled;type:tinyint(9);comment:是否启用,0:禁用,1:启用;default:1;" description:"是否启用,0:禁用,1:启用"`
+	SysUserId        int       `gorm:"column:sys_user_id;type:int(9) UNSIGNED;comment:用户id;default:0;" description:"用户id"`
+	ModifyTime       time.Time `gorm:"column:modify_time;type:datetime;comment:最后一次修改时间;default:NULL;" description:"最后一次修改时间"`
+	CreateTime       time.Time `gorm:"column:create_time;type:datetime;comment:添加时间;default:NULL;" description:"添加时间"`
+	FollowUserId     int       `gorm:"column:follow_user_id;type:int(9) UNSIGNED;comment:关注的用户id;default:0;" description:"关注的用户id"`
+}
+
+func (m *WechatPlatformUserMapping) GetListByCondition(condition string, pars []interface{}, startSize, pageSize int) (items []*UserFollowWechatPlatform, err error) {
+	sqlStr := fmt.Sprintf(`SELECT a.wechat_platform_id,a.fake_id,a.nickname,a.alias,a.round_head_img,a.service_type,a.signature,a.verified,a.article_link,a.enabled,a.sys_user_id,a.modify_time,a.create_time,b.sys_user_id as follow_user_id FROM wechat_platform a 
+    JOIN wechat_platform_user_mapping b on a.wechat_platform_id=b.wechat_platform_id 
+                                                                                                                                                                                                                   WHERE 1=1 %s LIMIT ?,?`, condition)
+	pars = append(pars, startSize, pageSize)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Find(&items).Error
+
+	return
+}
+
+func (m *WechatPlatformUserMapping) GetCountByCondition(condition string, pars []interface{}) (total int, err error) {
+	var intNull sql.NullInt64
+	sqlStr := fmt.Sprintf(`SELECT COUNT(1) total FROM wechat_platform a JOIN wechat_platform_user_mapping b on a.wechat_platform_id=b.wechat_platform_id WHERE 1=1 `, condition)
+	err = global.DbMap[utils.DbNameAI].Raw(sqlStr, pars...).Scan(&intNull).Error
+	if err == nil && intNull.Valid {
+		total = int(intNull.Int64)
+	}
+
+	return
+}
+
+func (m *WechatPlatformUserMapping) GetPageListByCondition(condition string, pars []interface{}, startSize, pageSize int) (total int, items []*UserFollowWechatPlatform, err error) {
+
+	total, err = m.GetCountByCondition(condition, pars)
+	if err != nil {
+		return
+	}
+	if total > 0 {
+		items, err = m.GetListByCondition(condition, pars, startSize, pageSize)
+	}
+
+	return
+}

+ 1 - 0
models/system/sys_user.go

@@ -23,6 +23,7 @@ type LoginResp struct {
 	SysRoleTypeCode string `description:"角色类型编码"`
 	AdminId         int    `description:"系统用户id"`
 	ProductName     string `description:"产品名称:admin,ficc,权益"`
+	Mobile          string `description:"手机号"`
 	Authority       int    `description:"管理权限,0:无,1:部门负责人,2:小组负责人,或者ficc销售主管,4:ficc销售组长"`
 }
 

+ 225 - 0
routers/commentsRouter.go

@@ -8350,6 +8350,231 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:AbstractController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:AbstractController"],
+        beego.ControllerComments{
+            Method: "Del",
+            Router: `/abstract/del`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:AbstractController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:AbstractController"],
+        beego.ControllerComments{
+            Method: "List",
+            Router: `/abstract/list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:AbstractController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:AbstractController"],
+        beego.ControllerComments{
+            Method: "AddVector",
+            Router: `/abstract/vector/add`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:AbstractController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:AbstractController"],
+        beego.ControllerComments{
+            Method: "VectorDel",
+            Router: `/abstract/vector/del`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:ChatWsController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:ChatWsController"],
+        beego.ControllerComments{
+            Method: "ChatConnect",
+            Router: `/chat/connect`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:KbController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:KbController"],
+        beego.ControllerComments{
+            Method: "SearchDocs",
+            Router: `/knowledge_base/searchDocs`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:QuestionController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:QuestionController"],
+        beego.ControllerComments{
+            Method: "Add",
+            Router: `/question/add`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:QuestionController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:QuestionController"],
+        beego.ControllerComments{
+            Method: "Del",
+            Router: `/question/del`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:QuestionController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:QuestionController"],
+        beego.ControllerComments{
+            Method: "Edit",
+            Router: `/question/edit`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:QuestionController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:QuestionController"],
+        beego.ControllerComments{
+            Method: "List",
+            Router: `/question/list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"],
+        beego.ControllerComments{
+            Method: "ChatRecordList",
+            Router: `/chat/chat_record_list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"],
+        beego.ControllerComments{
+            Method: "ChatRecordAdd",
+            Router: `/chat/chat_record_save`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"],
+        beego.ControllerComments{
+            Method: "DeleteChat",
+            Router: `/chat/delete_chat`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"],
+        beego.ControllerComments{
+            Method: "NewChat",
+            Router: `/chat/new_chat`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"],
+        beego.ControllerComments{
+            Method: "RenameChat",
+            Router: `/chat/rename_chat`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:UserChatController"],
+        beego.ControllerComments{
+            Method: "GetUserChatList",
+            Router: `/chat/user_chat_list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"],
+        beego.ControllerComments{
+            Method: "TagList",
+            Router: `/tag/list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"],
+        beego.ControllerComments{
+            Method: "Add",
+            Router: `/wechat_platform/add`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"],
+        beego.ControllerComments{
+            Method: "ArticleDel",
+            Router: `/wechat_platform/article/del`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"],
+        beego.ControllerComments{
+            Method: "ArticleDetail",
+            Router: `/wechat_platform/article/detail`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"],
+        beego.ControllerComments{
+            Method: "ArticleList",
+            Router: `/wechat_platform/article/list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"],
+        beego.ControllerComments{
+            Method: "FollowList",
+            Router: `/wechat_platform/list/follow`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"],
+        beego.ControllerComments{
+            Method: "PublicList",
+            Router: `/wechat_platform/list/public`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"],
+        beego.ControllerComments{
+            Method: "Op",
+            Router: `/wechat_platform/op`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/llm:WechatPlatformController"],
+        beego.ControllerComments{
+            Method: "Refresh",
+            Router: `/wechat_platform/refresh`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_api/controllers/material:MaterialController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/material:MaterialController"],
         beego.ControllerComments{
             Method: "BatchAdd",

+ 11 - 0
routers/router.go

@@ -30,6 +30,7 @@ import (
 	"eta/eta_api/controllers/eta_forum"
 	"eta/eta_api/controllers/eta_trial"
 	"eta/eta_api/controllers/fe_calendar"
+	"eta/eta_api/controllers/llm"
 	"eta/eta_api/controllers/material"
 	"eta/eta_api/controllers/report_approve"
 	"eta/eta_api/controllers/residual_analysis"
@@ -66,6 +67,16 @@ func init() {
 				&controllers.ClassifyController{},
 			),
 		),
+		web.NSNamespace("/llm",
+			web.NSInclude(
+				&llm.ChatWsController{},
+				&llm.UserChatController{},
+				&llm.KbController{},
+				&llm.WechatPlatformController{},
+				&llm.QuestionController{},
+				&llm.AbstractController{},
+			),
+		),
 		web.NSNamespace("/banner",
 			web.NSInclude(
 				&controllers.BannerController{},

+ 302 - 0
services/elastic/wechat_article.go

@@ -0,0 +1,302 @@
+package elastic
+
+import (
+	"context"
+	"encoding/json"
+	"eta/eta_api/models/rag"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/olivere/elastic/v7"
+	"time"
+)
+
+// WechatArticleAndPlatform
+// @Description: 存入ES的数据
+type WechatArticleAndPlatform struct {
+	WechatArticleId  int    `gorm:"column:wechat_article_id;type:int(10) UNSIGNED;primaryKey;not null;" description:""`
+	WechatPlatformId int    `gorm:"column:wechat_platform_id;type:int(11);comment:归属公众号id;default:0;" description:"归属公众号id"`
+	FakeId           string `gorm:"column:fake_id;type:varchar(255);comment:公众号唯一id;" description:"公众号唯一id"`
+	Title            string `gorm:"column:title;type:varchar(255);comment:标题;" description:"标题"`
+	Link             string `gorm:"column:link;type:varchar(255);comment:链接;" description:"链接"`
+	CoverUrl         string `gorm:"column:cover_url;type:varchar(255);comment:公众号封面;" description:"公众号封面"`
+	Description      string `gorm:"column:description;type:varchar(255);comment:描述;" description:"描述"`
+	Content          string `gorm:"column:content;type:longtext;comment:报告详情;" description:"报告详情"`
+	TextContent      string `gorm:"column:text_content;type:text;comment:文本内容;" description:"文本内容"`
+	//Abstract          string    `gorm:"column:abstract;type:text;comment:摘要;" description:"摘要"`
+	Country           string    `gorm:"column:country;type:varchar(255);comment:国家;" description:"国家"`
+	Province          string    `gorm:"column:province;type:varchar(255);comment:省;" description:"省"`
+	City              string    `gorm:"column:city;type:varchar(255);comment:市;" description:"市"`
+	ArticleCreateTime time.Time `gorm:"column:article_create_time;type:datetime;comment:报告创建时间;default:NULL;" description:"报告创建时间"`
+	IsDeleted         int       `gorm:"column:is_deleted;type:tinyint(4);comment:是否删除,0:未删除,1: 已删除;default:0;" description:"是否删除,0:未删除,1: 已删除"`
+	ModifyTime        time.Time `gorm:"column:modify_time;type:datetime;comment:修改时间;default:NULL;" description:"修改时间"`
+	CreateTime        time.Time `gorm:"column:create_time;type:datetime;comment:入库时间;default:NULL;" description:"入库时间"`
+	Nickname          string    `gorm:"column:nickname;type:varchar(255);comment:公众号名称;" description:"nickname"`          // 公众号名称
+	Alias             string    `gorm:"column:alias;type:varchar(255);comment:别名;" description:"alias"`                   // 别名
+	RoundHeadImg      string    `gorm:"column:round_head_img;type:varchar(255);comment:头像;" description:"round_head_img"` // 头像
+}
+
+func (m *WechatArticleAndPlatform) ToView() rag.WechatArticleView {
+	var articleCreateTime, modifyTime, createTime string
+
+	if !m.ArticleCreateTime.IsZero() {
+		articleCreateTime = m.ArticleCreateTime.Format(utils.FormatDateTime)
+	}
+	if !m.CreateTime.IsZero() {
+		createTime = m.CreateTime.Format(utils.FormatDateTime)
+	}
+	if !m.ModifyTime.IsZero() {
+		modifyTime = m.ModifyTime.Format(utils.FormatDateTime)
+	}
+	return rag.WechatArticleView{
+		WechatArticleId:  m.WechatArticleId,
+		WechatPlatformId: m.WechatPlatformId,
+		FakeId:           m.FakeId,
+		Title:            m.Title,
+		Link:             m.Link,
+		CoverUrl:         m.CoverUrl,
+		Description:      m.Description,
+		Content:          m.Content,
+		TextContent:      m.TextContent,
+		//Abstract:                   m.Abstract,
+		Country:                    m.Country,
+		Province:                   m.Province,
+		City:                       m.City,
+		ArticleCreateTime:          articleCreateTime,
+		ModifyTime:                 modifyTime,
+		CreateTime:                 createTime,
+		WechatPlatformName:         m.Nickname,
+		WechatPlatformRoundHeadImg: m.RoundHeadImg,
+	}
+}
+
+func (m *WechatArticleAndPlatform) ArticleAndPlatformListToViewList(list []*WechatArticleAndPlatform) (wechatArticleViewList []rag.WechatArticleView) {
+	wechatArticleViewList = make([]rag.WechatArticleView, 0)
+
+	for _, v := range list {
+		wechatArticleViewList = append(wechatArticleViewList, v.ToView())
+	}
+	return
+}
+
+// WechatArticleEsAddOrEdit
+// @Description: 新增/编辑微信文章
+// @author: Roc
+// @datetime 2025-03-13 10:24:05
+// @param docId string
+// @param item WechatArticleAndPlatform
+// @return err error
+func WechatArticleEsAddOrEdit(docId string, item WechatArticleAndPlatform) (err error) {
+	if docId == "" {
+		return
+	}
+	if utils.EsWechatArticleName == `` {
+		return
+	}
+	defer func() {
+		if err != nil {
+			fmt.Println("WechatArticleEsAddOrEdit Err:", err.Error())
+		}
+	}()
+	client := utils.EsClient
+
+	resp, err := client.Index().Index(utils.EsWechatArticleName).Id(docId).BodyJson(item).Refresh("true").Do(context.Background())
+	if err != nil {
+		fmt.Println("新增失败:", err.Error())
+		return err
+	}
+	if resp.Status == 0 {
+		fmt.Println("新增成功", resp.Result)
+		err = nil
+	} else {
+		fmt.Println("WechatArticleEsAddOrEdit", resp.Status, resp.Result)
+	}
+
+	return
+}
+
+// WechatArticleEsDel
+// @Description: 删除微信文章
+// @author: Roc
+// @datetime 2025-03-13 10:23:55
+// @param docId string
+// @return err error
+func WechatArticleEsDel(docId string) (err error) {
+	if docId == "" {
+		return
+	}
+	if utils.EsWechatArticleName == `` {
+		return
+	}
+	defer func() {
+		if err != nil {
+			fmt.Println("EsDeleteEdbInfoData Err:", err.Error())
+		}
+	}()
+	client := utils.EsClient
+
+	resp, err := client.Delete().Index(utils.EsWechatArticleName).Id(docId).Refresh(`true`).Do(context.Background())
+	if err != nil {
+		return
+	}
+	if resp.Status == 0 {
+		fmt.Println("删除成功")
+	} else {
+		fmt.Println("WechatArticleEsDel", resp.Status, resp.Result)
+	}
+
+	return
+}
+
+func WechatArticleEsSearch(keywordStr string, wechatPlatformId, from, size int, sortMap map[string]string) (total int64, list []*WechatArticleAndPlatform, err error) {
+	indexName := utils.EsWechatArticleName
+	list = make([]*WechatArticleAndPlatform, 0)
+	defer func() {
+		if err != nil {
+			fmt.Println("SearchEdbInfoData Err:", err.Error())
+		}
+	}()
+
+	query := elastic.NewBoolQuery()
+
+	if wechatPlatformId > 0 {
+		query = query.Must(elastic.NewTermQuery("WechatPlatformId", wechatPlatformId))
+	}
+
+	// 名字匹配
+	if keywordStr != `` {
+		query = query.Must(elastic.NewMultiMatchQuery(keywordStr, "Title"))
+	}
+
+	// 排序
+	sortList := make([]*elastic.FieldSort, 0)
+	// 如果没有关键字,那么就走指标id倒序
+
+	for orderKey, orderType := range sortMap {
+		switch orderType {
+		case "asc":
+			sortList = append(sortList, elastic.NewFieldSort(orderKey).Asc())
+		case "desc":
+			sortList = append(sortList, elastic.NewFieldSort(orderKey).Desc())
+
+		}
+
+	}
+
+	return searchWechatArticle(indexName, query, sortList, from, size)
+}
+
+// searchEdbInfoDataV2 查询es中的数据
+func searchWechatArticle(indexName string, query elastic.Query, sortList []*elastic.FieldSort, from, size int) (total int64, list []*WechatArticleAndPlatform, err error) {
+	total, err = searchWechatArticleTotal(indexName, query)
+	if err != nil {
+		return
+	}
+
+	// 获取列表数据
+	list, err = searchWechatArticleList(indexName, query, sortList, from, size)
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+// searchEdbInfoDataTotal
+// @Description: 查询es中的数量
+// @author: Roc
+// @datetime 2024-12-23 11:19:04
+// @param indexName string
+// @param query elastic.Query
+// @return total int64
+// @return err error
+func searchWechatArticleTotal(indexName string, query elastic.Query) (total int64, err error) {
+	defer func() {
+		if err != nil {
+			fmt.Println("searchEdbInfoDataTotal Err:", err.Error())
+		}
+	}()
+	client := utils.EsClient
+
+	//根据条件数量统计
+	requestTotalHits := client.Count(indexName).Query(query)
+	total, err = requestTotalHits.Do(context.Background())
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+// searchEdbInfoDataList
+// @Description: 查询es中的明细数据
+// @author: Roc
+// @datetime 2024-12-23 11:18:48
+// @param indexName string
+// @param query elastic.Query
+// @param sortList []*elastic.FieldSort
+// @param from int
+// @param size int
+// @return list []*data_manage.EdbInfoList
+// @return err error
+func searchWechatArticleList(indexName string, query elastic.Query, sortList []*elastic.FieldSort, from, size int) (list []*WechatArticleAndPlatform, err error) {
+	list = make([]*WechatArticleAndPlatform, 0)
+	defer func() {
+		if err != nil {
+			fmt.Println("searchEdbInfoDataList Err:", err.Error())
+		}
+	}()
+	client := utils.EsClient
+	// 高亮
+	highlight := elastic.NewHighlight()
+	highlight = highlight.Fields(elastic.NewHighlighterField("Title"))
+	highlight = highlight.PreTags("<font color='red'>").PostTags("</font>")
+
+	//request := client.Search(indexName).Highlight(highlight).From(from).Size(size) // sets the JSON request
+	request := client.Search(indexName).From(from).Size(size) // sets the JSON request
+
+	// 如果有指定排序,那么就按照排序来
+	if len(sortList) > 0 {
+		for _, v := range sortList {
+			request = request.SortBy(v)
+		}
+	}
+
+	searchMap := make(map[string]string)
+
+	searchResp, err := request.Query(query).Do(context.Background())
+	if err != nil {
+		return
+	}
+	//fmt.Println(searchResp)
+	//fmt.Println(searchResp.Status)
+	if searchResp.Status != 0 {
+		return
+	}
+	//total = searchResp.TotalHits()
+	if searchResp.Hits != nil {
+		for _, v := range searchResp.Hits.Hits {
+			if _, ok := searchMap[v.Id]; !ok {
+				itemJson, tmpErr := v.Source.MarshalJSON()
+				if tmpErr != nil {
+					err = tmpErr
+					fmt.Println("movieJson err:", err)
+					return
+				}
+				item := new(WechatArticleAndPlatform)
+				tmpErr = json.Unmarshal(itemJson, &item)
+				if tmpErr != nil {
+					fmt.Println("json.Unmarshal movieJson err:", tmpErr)
+					err = tmpErr
+					return
+				}
+				if len(v.Highlight["Title"]) > 0 {
+					item.Title = v.Highlight["Title"][0]
+				}
+				list = append(list, item)
+				searchMap[v.Id] = v.Id
+			}
+		}
+	}
+
+	return
+}

+ 317 - 0
services/elastic/wechat_article_abstract.go

@@ -0,0 +1,317 @@
+package elastic
+
+import (
+	"context"
+	"encoding/json"
+	"eta/eta_api/models/rag"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/olivere/elastic/v7"
+	"time"
+)
+
+// 摘要索引
+var EsWechatArticleAbstractName = utils.EsWechatArticleAbstractName
+
+type WechatArticleAbstractItem struct {
+	WechatArticleAbstractId int       `gorm:"column:wechat_article_abstract_id;type:int(9) UNSIGNED;primaryKey;not null;" description:"wechat_article_abstract_id"`
+	WechatArticleId         int       `gorm:"column:wechat_article_id;type:int(9) UNSIGNED;comment:关联的微信报告id;default:0;" description:"关联的微信报告id"`
+	WechatPlatformId        int       `gorm:"column:wechat_platform_id;type:int(9) UNSIGNED;comment:微信公众号id;default:0;" description:"微信公众号id"`
+	Abstract                string    `gorm:"column:abstract;type:longtext;comment:摘要内容;" description:"摘要内容"` //
+	Version                 int       `gorm:"column:version;type:int(10) UNSIGNED;comment:版本号;default:1;" description:"版本号"`
+	VectorKey               string    `gorm:"column:vector_key;type:varchar(255);comment:向量key标识;" description:"向量key标识"`
+	ModifyTime              time.Time `gorm:"column:modify_time;type:datetime;default:NULL;" description:"modify_time"`
+	CreateTime              time.Time `gorm:"column:create_time;type:datetime;default:NULL;" description:"create_time"`
+	Title                   string    `gorm:"column:title;type:varchar(255);comment:标题;" description:"标题"`
+	Link                    string    `gorm:"column:link;type:varchar(255);comment:链接;" description:"链接"`
+	TagIdList               []int     `description:"品种id列表"`
+}
+
+func (m *WechatArticleAbstractItem) ToView() rag.WechatArticleAbstractView {
+	var modifyTime, createTime string
+
+	if !m.CreateTime.IsZero() {
+		createTime = m.CreateTime.Format(utils.FormatDateTime)
+	}
+	if !m.ModifyTime.IsZero() {
+		modifyTime = m.ModifyTime.Format(utils.FormatDateTime)
+	}
+
+	tagId := 0
+	if len(m.TagIdList) > 0 {
+		tagId = m.TagIdList[0]
+	}
+	return rag.WechatArticleAbstractView{
+		WechatArticleAbstractId: m.WechatArticleAbstractId,
+		WechatArticleId:         m.WechatArticleId,
+		WechatPlatformId:        m.WechatPlatformId,
+		Abstract:                m.Abstract,
+		Version:                 m.Version,
+		VectorKey:               m.VectorKey,
+		ModifyTime:              modifyTime,
+		CreateTime:              createTime,
+		Title:                   m.Title,
+		Link:                    m.Link,
+		TagId:                   tagId,
+	}
+}
+
+func (m *WechatArticleAbstractItem) ToViewList(list []*WechatArticleAbstractItem) (wechatArticleViewList []rag.WechatArticleAbstractView) {
+	wechatArticleViewList = make([]rag.WechatArticleAbstractView, 0)
+
+	for _, v := range list {
+		wechatArticleViewList = append(wechatArticleViewList, v.ToView())
+	}
+	return
+}
+
+// WechatArticleEsAddOrEdit
+// @Description: 新增/编辑微信文章
+// @author: Roc
+// @datetime 2025-03-13 10:24:05
+// @param docId string
+// @param item WechatArticleAndPlatform
+// @return err error
+func WechatArticleAbstractEsAddOrEdit(docId string, item WechatArticleAbstractItem) (err error) {
+	if docId == "" {
+		return
+	}
+	if EsWechatArticleAbstractName == `` {
+		return
+	}
+	defer func() {
+		if err != nil {
+			fmt.Println("WechatArticleEsAddOrEdit Err:", err.Error())
+		}
+	}()
+	client := utils.EsClient
+
+	resp, err := client.Index().Index(EsWechatArticleAbstractName).Id(docId).BodyJson(item).Refresh("true").Do(context.Background())
+	if err != nil {
+		fmt.Println("新增失败:", err.Error())
+		return err
+	}
+	if resp.Status == 0 {
+		fmt.Println("新增成功", resp.Result)
+		err = nil
+	} else {
+		fmt.Println("WechatArticleEsAddOrEdit", resp.Status, resp.Result)
+	}
+
+	return
+}
+
+// WechatArticleEsDel
+// @Description: 删除微信文章
+// @author: Roc
+// @datetime 2025-03-13 10:23:55
+// @param docId string
+// @return err error
+func WechatArticleAbstractEsDel(docId string) (err error) {
+	if docId == "" {
+		return
+	}
+	if EsWechatArticleAbstractName == `` {
+		return
+	}
+	defer func() {
+		if err != nil {
+			fmt.Println("EsDeleteEdbInfoData Err:", err.Error())
+		}
+	}()
+	client := utils.EsClient
+
+	resp, err := client.Delete().Index(EsWechatArticleAbstractName).Id(docId).Refresh(`true`).Do(context.Background())
+	if err != nil {
+		return
+	}
+	if resp.Status == 0 {
+		fmt.Println("删除成功")
+	} else {
+		fmt.Println("WechatArticleEsDel", resp.Status, resp.Result)
+	}
+
+	return
+}
+
+// WechatArticleAbstractEsSearch
+// @Description: 搜索
+// @author: Roc
+// @datetime 2025-03-13 19:54:54
+// @param keywordStr string
+// @param tagIdList []int
+// @param platformIdList []int
+// @param from int
+// @param size int
+// @param sortMap map[string]string
+// @return total int64
+// @return list []*WechatArticleAbstractItem
+// @return err error
+func WechatArticleAbstractEsSearch(keywordStr string, tagIdList, platformIdList []int, from, size int, sortMap map[string]string) (total int64, list []*WechatArticleAbstractItem, err error) {
+	indexName := EsWechatArticleAbstractName
+	list = make([]*WechatArticleAbstractItem, 0)
+	defer func() {
+		if err != nil {
+			fmt.Println("SearchEdbInfoData Err:", err.Error())
+		}
+	}()
+
+	query := elastic.NewBoolQuery()
+
+	if len(tagIdList) > 0 {
+		termsList := make([]interface{}, 0)
+		for _, v := range tagIdList {
+			termsList = append(termsList, v)
+		}
+		query = query.Must(elastic.NewTermsQuery("TagIdList", termsList...))
+	}
+	if len(platformIdList) <= 0 {
+		return
+	}
+
+	{
+		termsList := make([]interface{}, 0)
+		for _, v := range platformIdList {
+			termsList = append(termsList, v)
+		}
+		query = query.Must(elastic.NewTermsQuery("WechatPlatformId", termsList...))
+	}
+
+	// 名字匹配
+	if keywordStr != `` {
+		query = query.Must(elastic.NewMultiMatchQuery(keywordStr, "Abstract"))
+	}
+
+	// 排序
+	sortList := make([]*elastic.FieldSort, 0)
+	// 如果没有关键字,那么就走指标id倒序
+
+	for orderKey, orderType := range sortMap {
+		switch orderType {
+		case "asc":
+			sortList = append(sortList, elastic.NewFieldSort(orderKey).Asc())
+		case "desc":
+			sortList = append(sortList, elastic.NewFieldSort(orderKey).Desc())
+
+		}
+
+	}
+
+	return searchWechatArticleAbstract(indexName, query, sortList, from, size)
+}
+
+// searchEdbInfoDataV2 查询es中的数据
+func searchWechatArticleAbstract(indexName string, query elastic.Query, sortList []*elastic.FieldSort, from, size int) (total int64, list []*WechatArticleAbstractItem, err error) {
+	total, err = searchWechatArticleAbstractTotal(indexName, query)
+	if err != nil {
+		return
+	}
+
+	// 获取列表数据
+	list, err = searchWechatArticleAbstractList(indexName, query, sortList, from, size)
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+// searchEdbInfoDataTotal
+// @Description: 查询es中的数量
+// @author: Roc
+// @datetime 2024-12-23 11:19:04
+// @param indexName string
+// @param query elastic.Query
+// @return total int64
+// @return err error
+func searchWechatArticleAbstractTotal(indexName string, query elastic.Query) (total int64, err error) {
+	defer func() {
+		if err != nil {
+			fmt.Println("searchEdbInfoDataTotal Err:", err.Error())
+		}
+	}()
+	client := utils.EsClient
+
+	//根据条件数量统计
+	requestTotalHits := client.Count(indexName).Query(query)
+	total, err = requestTotalHits.Do(context.Background())
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+// searchEdbInfoDataList
+// @Description: 查询es中的明细数据
+// @author: Roc
+// @datetime 2024-12-23 11:18:48
+// @param indexName string
+// @param query elastic.Query
+// @param sortList []*elastic.FieldSort
+// @param from int
+// @param size int
+// @return list []*data_manage.EdbInfoList
+// @return err error
+func searchWechatArticleAbstractList(indexName string, query elastic.Query, sortList []*elastic.FieldSort, from, size int) (list []*WechatArticleAbstractItem, err error) {
+	list = make([]*WechatArticleAbstractItem, 0)
+	defer func() {
+		if err != nil {
+			fmt.Println("searchEdbInfoDataList Err:", err.Error())
+		}
+	}()
+	client := utils.EsClient
+	// 高亮
+	highlight := elastic.NewHighlight()
+	highlight = highlight.Fields(elastic.NewHighlighterField("Content"))
+	highlight = highlight.PreTags("<font color='red'>").PostTags("</font>")
+
+	//request := client.Search(indexName).Highlight(highlight).From(from).Size(size) // sets the JSON request
+	request := client.Search(indexName).From(from).Size(size) // sets the JSON request
+
+	// 如果有指定排序,那么就按照排序来
+	if len(sortList) > 0 {
+		for _, v := range sortList {
+			request = request.SortBy(v)
+		}
+	}
+
+	searchMap := make(map[string]string)
+
+	searchResp, err := request.Query(query).Do(context.Background())
+	if err != nil {
+		return
+	}
+	//fmt.Println(searchResp)
+	//fmt.Println(searchResp.Status)
+	if searchResp.Status != 0 {
+		return
+	}
+	//total = searchResp.TotalHits()
+	if searchResp.Hits != nil {
+		for _, v := range searchResp.Hits.Hits {
+			if _, ok := searchMap[v.Id]; !ok {
+				itemJson, tmpErr := v.Source.MarshalJSON()
+				if tmpErr != nil {
+					err = tmpErr
+					fmt.Println("movieJson err:", err)
+					return
+				}
+				item := new(WechatArticleAbstractItem)
+				tmpErr = json.Unmarshal(itemJson, &item)
+				if tmpErr != nil {
+					fmt.Println("json.Unmarshal movieJson err:", tmpErr)
+					err = tmpErr
+					return
+				}
+				if len(v.Highlight["Content"]) > 0 {
+					item.Abstract = v.Highlight["Content"][0]
+				}
+				list = append(list, item)
+				searchMap[v.Id] = v.Id
+			}
+		}
+	}
+
+	return
+}

+ 212 - 0
services/llm/base_wechat_lib.go

@@ -0,0 +1,212 @@
+package llm
+
+import (
+	"encoding/json"
+	"eta/eta_api/utils"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+)
+
+type BaseResp struct {
+	Code int    `json:"code"`
+	Msg  string `json:"msg"`
+}
+
+type WechatPlatformListResp struct {
+	BaseResp
+	Data []WechatPlatformResp `json:"data"`
+}
+
+type WechatPlatformResp struct {
+	Fakeid       string `json:"fakeid"`
+	Nickname     string `json:"nickname"`
+	Alias        string `json:"alias"`
+	RoundHeadImg string `json:"round_head_img"`
+	ServiceType  int    `json:"service_type"`
+	Signature    string `json:"signature"`
+	Verified     bool   `json:"verified"`
+}
+
+// SearchByWechat
+// @Description: 公众号列表
+// @author: Roc
+// @datetime 2025-03-04 18:09:01
+// @param name string
+// @return resp WechatPlatformResp
+// @return err error
+func SearchByWechat(name string) (items []WechatPlatformResp, err error) {
+	if utils.ETA_WX_CRAWLER_URL == "" {
+		err = fmt.Errorf("ETA微信爬虫服务地址为空")
+		return
+	}
+	getUrl := utils.ETA_WX_CRAWLER_URL + `/api/wechat_platform/search_by_wechat?name=` + name
+	result, err := HttpGet(getUrl)
+	if err != nil {
+		err = fmt.Errorf("调用ETA微信爬虫服务接口失败 error:%s", err.Error())
+		return
+	}
+
+	var resp WechatPlatformListResp
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	items = resp.Data
+
+	return
+}
+
+type WechatArticleResp struct {
+	BaseResp
+	Data WechatArticleDataResp `json:"data"`
+}
+
+type WechatArticleDataResp struct {
+	HtmlContent      string `json:"HtmlContent"`
+	TextContent      string `json:"TextContent"`
+	RoundHeadImg     string `json:"RoundHeadImg"`
+	ProfileSignature string `json:"ProfileSignature"`
+	Appuin           string `json:"Appuin"`
+	Nickname         string `json:"Nickname"`
+	UserName         string `json:"UserName"`
+	Title            string `json:"Title"`
+	Desc             string `json:"Desc"`
+	CoverUrl         string `json:"CoverUrl"`
+	CreateAt         string `json:"CreateAt"`
+	CountryName      string `json:"CountryName"`
+	ProvinceName     string `json:"ProvinceName"`
+	CityName         string `json:"CityName"`
+}
+
+// SearchByWechatArticle
+// @Description: 获取报告详情
+// @author: Roc
+// @datetime 2025-03-04 18:08:45
+// @param link string
+// @return resp WechatArticleResp
+// @return err error
+func SearchByWechatArticle(link string) (wechatArticle WechatArticleDataResp, err error) {
+	if utils.ETA_WX_CRAWLER_URL == "" {
+		err = fmt.Errorf("ETA微信爬虫服务地址为空")
+		return
+	}
+	getUrl := utils.ETA_WX_CRAWLER_URL + `/api/wechat_platform/article/info/search_by_wechat?link=` + link
+	result, err := HttpGet(getUrl)
+	if err != nil {
+		err = fmt.Errorf("调用ETA微信爬虫服务接口失败 error:%s", err.Error())
+		return
+	}
+
+	var resp WechatArticleResp
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	wechatArticle = resp.Data
+
+	return
+}
+
+type WechatArticleListResp struct {
+	BaseResp
+	Data WechatArticleMenuPage `json:"data"`
+}
+
+type WechatArticleMenuPage struct {
+	List  []ArticleMenu `json:"List"`
+	Total int           `json:"Total"`
+}
+
+type ArticleMenu struct {
+	Aid        string `json:"Aid"`
+	Title      string `json:"Title"`
+	Link       string `json:"Link"`
+	Cover      string `json:"Cover"`
+	Digest     string `json:"Digest"`
+	UpdateTime int    `json:"UpdateTime"`
+	CreateTime int    `json:"CreateTime"`
+	AppMsgId   int64  `json:"AppMsgId"`
+	AuthorName string `json:"AuthorName"`
+	Content    string `json:"Content"`
+}
+
+func SearchByWechatArticleList(fakeId string, num int) (items WechatArticleMenuPage, err error) {
+	if utils.ETA_WX_CRAWLER_URL == "" {
+		err = fmt.Errorf("ETA微信爬虫服务地址为空")
+		return
+	}
+	getUrl := fmt.Sprintf(`%s/api/wechat_platform/article/list/search_by_wechat?fakeid=%s&num=%d`, utils.ETA_WX_CRAWLER_URL, fakeId, num)
+	result, err := HttpGet(getUrl)
+	if err != nil {
+		err = fmt.Errorf("调用ETA微信爬虫服务接口失败 error:%s", err.Error())
+		return
+	}
+
+	var resp WechatArticleListResp
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	items = resp.Data
+
+	return
+}
+
+func HttpGet(url string) ([]byte, error) {
+	client := &http.Client{}
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("authorization", utils.MD5(utils.ETA_FORUM_HUB_NAME_EN+utils.ETA_FORUM_HUB_MD5_KEY))
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	result, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	utils.FileLog.Debug("HttpPost:" + string(result))
+
+	var baseResp BaseResp
+	err = json.Unmarshal(result, &baseResp)
+	if err != nil {
+		return nil, err
+	}
+	if baseResp.Code != 200 {
+		return nil, fmt.Errorf("code:%d,msg:%s", baseResp.Code, baseResp.Msg)
+	}
+
+	return result, err
+}
+
+func HttpPost(url, postData, lang string, params ...string) ([]byte, error) {
+	body := io.NopCloser(strings.NewReader(postData))
+	client := &http.Client{}
+	req, err := http.NewRequest("POST", url, body)
+	if err != nil {
+		return nil, err
+	}
+	contentType := "application/x-www-form-urlencoded;charset=utf-8"
+	if len(params) > 0 && params[0] != "" {
+		contentType = params[0]
+	}
+	req.Header.Set("Content-Type", contentType)
+	req.Header.Set("Lang", lang)
+	req.Header.Set("authorization", utils.MD5(utils.APP_EDB_LIB_NAME_EN+utils.EDB_LIB_Md5_KEY))
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	b, err := io.ReadAll(resp.Body)
+	utils.FileLog.Debug("HttpPost:" + string(b))
+	return b, err
+}

+ 304 - 0
services/llm/chat.go

@@ -0,0 +1,304 @@
+package llm
+
+import (
+	"bytes"
+	"encoding/json"
+	"eta/eta_api/utils"
+	"eta/eta_api/utils/llm/eta_llm"
+	"eta/eta_api/utils/llm/eta_llm/eta_llm_http"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"os"
+	"strings"
+)
+
+var (
+	llmService = eta_llm.GetInstance()
+)
+
+type UploadTempDocsResp struct {
+	Code int                   `json:"code"`
+	Msg  string                `json:"msg"`
+	Data UploadTempDocDataResp `json:"data"`
+}
+
+type UploadTempDocDataResp struct {
+	Id          string        `json:"id"`
+	FailedFiles []interface{} `json:"failed_files"`
+}
+
+// UploadTempDocs
+// @Description: 上传到临时知识库
+// @author: Roc
+// @datetime 2025-03-10 14:42:18
+// @param filePath string
+// @return resp UploadDocsResp
+// @return err error
+func UploadTempDocs(filePath string) (resp UploadTempDocsResp, err error) {
+	postUrl := utils.LLM_SERVER + "/knowledge_base/upload_temp_docs"
+
+	params := make(map[string]string)
+	//params[`prev_id`] = ``
+	params[`chunk_size`] = `750`
+	params[`chunk_overlap`] = `150`
+	params[`zh_title_enhance`] = `true`
+
+	files := make(map[string]string)
+	files[`files`] = filePath
+
+	result, err := PostFormData(postUrl, params, files)
+	if err != nil {
+		return
+	}
+
+	str := string(result)
+	fmt.Println(str)
+
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+type LlmBaseResp struct {
+	Code int    `json:"code"`
+	Msg  string `json:"msg"`
+}
+
+type UploadDocsResp struct {
+	LlmBaseResp
+	Data UploadDocDataResp `json:"data"`
+}
+
+type UploadDocDataResp struct {
+	Id          string            `json:"id"`
+	FailedFiles map[string]string `json:"failed_files"`
+}
+
+// UploadDocsToKnowledge
+// @Description: 上传文章到知识库
+// @author: Roc
+// @datetime 2025-03-10 14:40:44
+// @param filePath string
+// @param knowledgeName string
+// @return resp UploadTempDocsResp
+// @return err error
+func UploadDocsToKnowledge(filePath, knowledgeName string) (updateResp UploadDocDataResp, err error) {
+	postUrl := utils.LLM_SERVER + "/knowledge_base/upload_docs"
+
+	params := make(map[string]string)
+	params[`knowledge_base_name`] = knowledgeName
+	params[`override`] = `true`              // 覆盖已有文件
+	params[`to_vector_store`] = `true`       // 上传文件后是否进行向量化
+	params[`chunk_size`] = `750`             // 知识库中单段文本最大长度
+	params[`chunk_overlap`] = `150`          // 知识库中相邻文本重合长度
+	params[`zh_title_enhance`] = `true`      // 是否开启中文标题加强
+	params[`docs`] = ``                      // 自定义的docs,需要转为json字符串
+	params[`not_refresh_vs_cache`] = `false` // 暂不保存向量库(用于FAISS)
+
+	files := make(map[string]string)
+	files[`files`] = filePath
+
+	result, err := PostFormData(postUrl, params, files)
+	if err != nil {
+		return
+	}
+
+	str := string(result)
+	fmt.Println(str)
+
+	var resp UploadDocsResp
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.Code != 200 {
+		err = fmt.Errorf(`上传文件失败: %s`, resp.Msg)
+		return
+	}
+	updateResp = resp.Data
+
+	return
+}
+
+// DelDocsToKnowledge
+// @Description: 从知识库中删除文件
+// @author: Roc
+// @datetime 2025-03-12 15:03:19
+// @param knowledgeName string
+// @param filePathList []string
+// @return resp LlmBaseResp
+// @return err error
+func DelDocsToKnowledge(knowledgeName string, filePathList []string) (resp LlmBaseResp, err error) {
+	postUrl := utils.LLM_SERVER + "/knowledge_base/delete_docs"
+
+	params := make(map[string]interface{})
+	params[`knowledge_base_name`] = knowledgeName
+	params[`file_names`] = filePathList
+	params[`delete_content`] = `true`        //
+	params[`not_refresh_vs_cache`] = `false` //
+	postData, err := json.Marshal(params)
+	if err != nil {
+		return
+	}
+
+	result, err := LlmHttpPost(postUrl, string(postData))
+	if err != nil {
+		return
+	}
+	utils.FileLog.Info("DelDocsToKnowledge:" + postUrl + ";" + string(postData) + ";result:" + string(result))
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.Code != 200 {
+		err = fmt.Errorf(`上传文件失败: %s`, resp.Msg)
+		return
+	}
+
+	return
+}
+
+// ChatResp 问答响应
+type ChatResp struct {
+	Answer string   `json:"answer"`
+	Docs   []string `json:"docs"`
+}
+
+type HistoryContent struct {
+	Content string `json:"content"`
+	Role    string `json:"role"`
+}
+
+func ChatByFile(knowledgeId, question string, historyList []eta_llm_http.HistoryContent) (answerStr string, answer ChatResp, err error) {
+	// 没有问题那就直接返回
+	if question == `` {
+		return
+	}
+
+	history := make([]json.RawMessage, 0)
+	for _, v := range historyList {
+		tmpHistory, tmpErr := json.Marshal(v)
+		if tmpErr != nil {
+			return
+		}
+		history = append(history, json.RawMessage(string(tmpHistory)))
+	}
+
+	resp, err := llmService.DocumentChat(question, knowledgeId, history, false)
+	if err != nil {
+		return
+	}
+	defer func() {
+		if resp != nil && resp.Body != nil {
+			_ = resp.Body.Close()
+		}
+	}()
+	if resp == nil {
+		err = fmt.Errorf(`知识库问答失败: 无应答`)
+		return
+	}
+	result, err := io.ReadAll(resp.Body)
+	if err != nil {
+		err = fmt.Errorf(`知识库问答数据解析失败: %s`, err.Error())
+		return
+	}
+
+	answerStr = string(result)
+	// 找到"data:"关键字的位置
+	dataIndex := bytes.Index([]byte(answerStr), []byte("data: "))
+	if dataIndex == -1 {
+		err = fmt.Errorf(`未找到"data:"关键字`)
+		return
+	}
+
+	// 提取"data:"关键字之后的部分
+	answerStr = answerStr[dataIndex+len("data: "):]
+
+	// 解析JSON数据
+	err = json.Unmarshal([]byte(answerStr), &answer)
+	if err != nil {
+		err = fmt.Errorf(`解析JSON数据失败: %s`, err.Error())
+		return
+	}
+
+	return
+}
+
+// PostFormData sends a POST request with form-data
+func PostFormData(url string, params map[string]string, files map[string]string) ([]byte, error) {
+
+	body := &bytes.Buffer{}
+	writer := multipart.NewWriter(body)
+
+	for key, val := range params {
+		if err := writer.WriteField(key, val); err != nil {
+			return nil, err
+		}
+	}
+
+	for fieldName, filePath := range files {
+		file, err := os.Open(filePath)
+		if err != nil {
+			return nil, err
+		}
+		defer file.Close()
+
+		part, err := writer.CreateFormFile(fieldName, filePath)
+		if err != nil {
+			return nil, err
+		}
+		_, err = io.Copy(part, file)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	err := writer.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequest("POST", url, body)
+	if err != nil {
+		return nil, err
+	}
+	//req.Header.Set("accept", `application/json`)
+	req.Header.Set("Content-Type", writer.FormDataContentType())
+
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	result, err := io.ReadAll(resp.Body)
+
+	return result, nil
+}
+
+func LlmHttpPost(url, postData string) ([]byte, error) {
+	body := io.NopCloser(strings.NewReader(postData))
+	client := &http.Client{}
+	req, err := http.NewRequest("POST", url, body)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("authorization", utils.MD5(utils.APP_EDB_LIB_NAME_EN+utils.EDB_LIB_Md5_KEY))
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	b, err := io.ReadAll(resp.Body)
+	utils.FileLog.Debug("HttpPost:" + string(b))
+	return b, err
+}

+ 213 - 0
services/llm/chat_service.go

@@ -0,0 +1,213 @@
+package llm
+
+import (
+	"encoding/json"
+	"eta/eta_api/global"
+	"eta/eta_api/models/llm"
+	"eta/eta_api/utils"
+	"eta/eta_api/utils/lock"
+	"eta/eta_api/utils/redis"
+	"fmt"
+	"github.com/google/uuid"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+)
+
+const (
+	redisChatPrefix = "chat:zet:"
+	redisTTL        = 24 * time.Hour // Redis 缓存过期时间
+)
+
+// AddChatRecord 添加聊天记录到 Redis
+func AddChatRecord(record *llm.UserChatRecordRedis) error {
+	key := fmt.Sprintf("%s%d", redisChatPrefix, record.ChatId)
+	holder, _ := uuid.NewRandom()
+	holderStr := fmt.Sprintf("user_%s", holder.String())
+	if err := lock.TryLock(key, 10, holderStr, 10*time.Second); err == nil {
+		defer func() {
+			lock.ReleaseLock(key, holderStr)
+		}()
+		data, parseErr := json.Marshal(record)
+		if parseErr != nil {
+			return fmt.Errorf("序列化聊天记录失败: %w", parseErr)
+		}
+		zSet, _ := utils.Rc.ZRangeWithScores(key)
+		if len(zSet) == 0 {
+			// 设置过期时间
+			_ = utils.Rc.Expire(key, 24*time.Hour)
+		}
+		zSet = append(zSet, &redis.Zset{
+			Member: data,
+			Score:  float64(time.Now().Unix()),
+		})
+
+		err = utils.Rc.ZAdd(key, zSet...)
+		if err != nil {
+			return fmt.Errorf("保存聊天记录到 Redis 失败: %w", err)
+		}
+		return nil
+	}
+	return fmt.Errorf("获取锁失败,请稍后重试")
+}
+
+// GetChatRecordsFromRedis 从 Redis 获取聊天记录
+func GetChatRecordsFromRedis(chatId int) (redisList []*llm.UserChatRecordRedis, err error) {
+	key := fmt.Sprintf("%s%d", redisChatPrefix, chatId)
+	zSet, _ := utils.Rc.ZRangeWithScores(key)
+	if len(zSet) == 0 {
+		// 缓存不存在,从数据库拉取数据
+		records, dbErr := GetChatRecordsFromDB(chatId)
+		if dbErr != nil {
+			err = fmt.Errorf("从数据库获取聊天记录失败: %w", dbErr)
+			return
+		}
+		// 将数据保存到 Redis
+		for _, record := range records {
+			redisRecord := &llm.UserChatRecordRedis{
+				Id:           record.Id,
+				ChatId:       chatId,
+				ChatUserType: record.ChatUserType,
+				Content:      record.Content,
+				SendTime:     record.SendTime.Format(utils.FormatDateTime),
+			}
+			redisList = append(redisList, redisRecord)
+		}
+		return
+	}
+	for _, z := range zSet {
+		var redisRecord llm.UserChatRecordRedis
+		if err = json.Unmarshal([]byte(z.Member.(string)), &redisRecord); err != nil {
+			return nil, fmt.Errorf("解析聊天记录失败: %w", err)
+		}
+		redisList = append(redisList, &redisRecord)
+	}
+	return
+}
+
+func flushRecordsToRedis(chatId int) (err error) {
+	key := fmt.Sprintf("%s%d", redisChatPrefix, chatId)
+	zSet, _ := utils.Rc.ZRangeWithScores(key)
+	if len(zSet) == 0 {
+		// 缓存不存在,从数据库拉取数据
+		records, dbErr := GetChatRecordsFromDB(chatId)
+		if dbErr != nil {
+			err = fmt.Errorf("从数据库获取聊天记录失败: %w", dbErr)
+			return
+		}
+		var zet []*redis.Zset
+		// 将数据保存到 Redis
+		for _, record := range records {
+			redisRecord := &llm.UserChatRecordRedis{
+				Id:           record.Id,
+				ChatId:       chatId,
+				ChatUserType: record.ChatUserType,
+				Content:      record.Content,
+				SendTime:     record.SendTime.Format(utils.FormatDateTime),
+			}
+			data, parseErr := json.Marshal(&redisRecord)
+			if parseErr != nil {
+				utils.FileLog.Error("解析聊天记录失败: %w", err)
+			}
+			zet = append(zet, &redis.Zset{
+				Member: data,
+				Score:  float64(record.SendTime.Unix()),
+			})
+		}
+		_ = utils.Rc.ZAdd(key, zet...)
+	}
+	return
+}
+
+// SaveChatRecordsToDB 将 Redis 中的聊天记录保存到数据库
+func SaveChatRecordsToDB(chatId int) error {
+	list, err := GetChatRecordsFromRedis(chatId)
+	if err != nil {
+		return err
+	}
+	var newRecords []*llm.UserChatRecord
+	for _, record := range list {
+		if record.Id == 0 {
+			sendTime, parseErr := time.ParseInLocation(utils.FormatDateTime, record.SendTime, time.Local)
+			if parseErr != nil {
+				sendTime = time.Now()
+			}
+			newRecords = append(newRecords, &llm.UserChatRecord{
+				Id:           record.Id,
+				ChatId:       record.ChatId,
+				ChatUserType: record.ChatUserType,
+				Content:      record.Content,
+				SendTime:     sendTime,
+				CreatedTime:  time.Now(),
+			})
+		}
+	}
+	key := fmt.Sprintf("%s%d", redisChatPrefix, chatId)
+	holder, _ := uuid.NewRandom()
+	holderStr := fmt.Sprintf("sys_%s", holder.String())
+	defer func() {
+		lock.ReleaseLock(key, holderStr)
+	}()
+	if lock.AcquireLock(key, 10, holderStr) {
+		//先删除redis中的缓存
+		_ = RemoveChatRecord(chatId)
+		err = llm.BatchInsertRecords(newRecords)
+		if err != nil {
+			utils.FileLog.Error("批量插入记录失败:", err.Error())
+			return fmt.Errorf("批量插入记录失败: %w", err)
+		}
+		_ = RemoveChatRecord(chatId)
+		//重新加载数据
+		_ = flushRecordsToRedis(chatId)
+	}
+	return nil
+}
+
+// SaveAllChatRecordsToDB 定时任务保存所有 Redis 中的聊天记录到数据库
+func SaveAllChatRecordsToDB() {
+	for {
+		keys, err := utils.Rc.Keys(redisChatPrefix + "*")
+		if err != nil {
+			utils.FileLog.Error("获取 Redis 键失败: %v", err)
+			return
+		}
+		var wg sync.WaitGroup
+		wg.Add(len(keys))
+		for _, key := range keys {
+			go func(key string) {
+				defer wg.Done()
+				chatIdStr := strings.TrimPrefix(key, redisChatPrefix)
+				chatId, parseErr := strconv.Atoi(chatIdStr)
+				if parseErr != nil {
+					utils.FileLog.Error("解析聊天ID失败: %v", err)
+					return
+				}
+				if err = SaveChatRecordsToDB(chatId); err != nil {
+					utils.FileLog.Error("解析聊天ID失败: %v", err)
+				}
+			}(key)
+		}
+		wg.Wait()
+		time.Sleep(10 * time.Minute)
+	}
+}
+
+// RemoveChatRecord 从 Redis 删除聊天记录
+func RemoveChatRecord(chatId int) error {
+	key := fmt.Sprintf("%s%d", redisChatPrefix, chatId)
+	err := utils.Rc.Delete(key)
+	if err != nil {
+		return fmt.Errorf("删除 Redis 缓存失败: %w", err)
+	}
+	return nil
+}
+
+func GetChatRecordsFromDB(chatId int) ([]*llm.UserChatRecord, error) {
+	o := global.DbMap[utils.DbNameAI]
+	var records []*llm.UserChatRecord
+	if err := o.Where("chat_id = ?", chatId).Find(&records).Error; err != nil {
+		return nil, fmt.Errorf("从数据库获取聊天记录失败: %w", err)
+	}
+	return records, nil
+}

+ 11 - 0
services/llm/facade/bus_response/bus_response.go

@@ -0,0 +1,11 @@
+package bus_response
+
+import "eta/eta_api/utils/llm/eta_llm/eta_llm_http"
+
+type KnowledgeBaseChatResponse struct {
+	PageContent string                `json:"page_content"`
+	Metadata    eta_llm_http.Metadata `json:"metadata"`
+	Type        string                `json:"type"`
+	Id          string                `json:"id"`
+	Score       float32               `json:"score"`
+}

+ 8 - 0
services/llm/facade/bus_response/eta_response.go

@@ -0,0 +1,8 @@
+package bus_response
+
+import "eta/eta_api/utils/llm/eta_llm/eta_llm_http"
+
+type SearchDocsEtaResponse struct {
+	Content string
+	Docs    []eta_llm_http.SearchDocsResponse
+}

+ 44 - 0
services/llm/facade/llm_service.go

@@ -0,0 +1,44 @@
+package facade
+
+import (
+	"eta/eta_api/services/llm/facade/bus_response"
+	"eta/eta_api/utils/llm"
+	"eta/eta_api/utils/llm/eta_llm/eta_llm_http"
+	"eta/eta_api/utils/ws"
+	"fmt"
+	"github.com/gorilla/websocket"
+	"github.com/rdlucklib/rdluck_tools/uuid"
+)
+
+var (
+	llmService, _ = llm.GetInstance(llm.ETA_LLM_CLIENT)
+)
+
+func generateSessionCode() (code string) {
+	return fmt.Sprintf("%s%s", "llm_session_", uuid.NewUUID().Hex32())
+}
+
+// AddSession 创建会话session
+func AddSession(userId int, conn *websocket.Conn) {
+	sessionId := generateSessionCode()
+	session := ws.NewSession(userId, sessionId, conn)
+	ws.Manager().AddSession(session)
+}
+
+// LLMKnowledgeBaseSearchDocs 搜索知识库
+func LLMKnowledgeBaseSearchDocs(search LLMKnowledgeSearch) (resp bus_response.SearchDocsEtaResponse, err error) {
+	docs, err := llmService.SearchKbDocs(search.Query, search.KnowledgeBaseName)
+	if err != nil {
+		return
+	}
+	for _, doc := range docs.([]eta_llm_http.SearchDocsResponse) {
+		resp.Content = resp.Content + doc.PageContent
+	}
+	resp.Docs = docs.([]eta_llm_http.SearchDocsResponse)
+	return
+}
+
+type LLMKnowledgeSearch struct {
+	Query             string `json:"Query"`
+	KnowledgeBaseName string `json:"KnowledgeBaseName"`
+}

+ 76 - 0
services/task.go

@@ -1,10 +1,14 @@
 package services
 
 import (
+	"encoding/json"
+	"eta/eta_api/cache"
 	"eta/eta_api/models"
+	"eta/eta_api/models/rag"
 	"eta/eta_api/services/binlog"
 	"eta/eta_api/services/data"
 	edbmonitor "eta/eta_api/services/edb_monitor"
+	"eta/eta_api/services/llm"
 	"eta/eta_api/utils"
 	"fmt"
 	"strings"
@@ -60,6 +64,15 @@ func Task() {
 		// 监听数据源binlog写入es
 		go binlog.HandleDataSourceChange2Es()
 	}
+	go StartSessionManager()
+
+	go llm.SaveAllChatRecordsToDB()
+
+	// 定时任务进行微信文章操作
+	go HandleWechatArticleOp()
+
+	// 定时任务进行微信文章LLM操作
+	go HandleWechatArticleLLmOp()
 
 	// TODO:数据修复
 	//FixNewEs()
@@ -569,3 +582,66 @@ func ModifyEsEnglishReport() {
 //
 //	return rnd.Float64()*11000 - 1000
 //}
+
+// HandleSearchByWechatOp
+// @Description: 处理微信爬虫
+func HandleWechatArticleOp() {
+	defer func() {
+		if err := recover(); err != nil {
+			fmt.Println("[HandleWechatArticleOp]", err)
+		}
+	}()
+	obj := rag.WechatPlatform{}
+	for {
+		utils.Rc.Brpop(utils.CACHE_WECHAT_PLATFORM_ARTICLE, func(b []byte) {
+			wechatArticleOp := new(cache.WechatArticleOp)
+			if err := json.Unmarshal(b, &wechatArticleOp); err != nil {
+				fmt.Println("json unmarshal wrong!")
+				return
+			}
+			item, tmpErr := obj.GetById(wechatArticleOp.WechatPlatformId)
+			if tmpErr != nil {
+				// 找不到就处理失败
+				return
+			}
+
+			switch wechatArticleOp.Source {
+			case `add`:
+				AddWechatPlatform(item)
+			case `refresh`:
+				BeachAddWechatArticle(item, 2)
+
+			}
+		})
+	}
+}
+
+// HandleWechatArticleLLmOp
+// @Description: 处理微信文章加入知识库
+func HandleWechatArticleLLmOp() {
+	defer func() {
+		if err := recover(); err != nil {
+			fmt.Println("[HandleWechatArticleLLmOp]", err)
+		}
+	}()
+	obj := rag.WechatArticle{}
+	for {
+		utils.Rc.Brpop(utils.CACHE_WECHAT_PLATFORM_ARTICLE_KNOWLEDGE, func(b []byte) {
+			wechatArticleOp := new(cache.WechatArticleOp)
+			if err := json.Unmarshal(b, &wechatArticleOp); err != nil {
+				fmt.Println("json unmarshal wrong!")
+				return
+			}
+			item, tmpErr := obj.GetById(wechatArticleOp.WechatPlatformId)
+			if tmpErr != nil {
+				// 找不到就处理失败
+				return
+			}
+
+			// 文章加入到知识库
+			ArticleToKnowledge(item)
+			// 生成摘要
+			//GenerateArticleAbstract(item)
+		})
+	}
+}

+ 1030 - 0
services/wechat_platform.go

@@ -0,0 +1,1030 @@
+package services
+
+import (
+	"bytes"
+	"eta/eta_api/cache"
+	"eta/eta_api/models"
+	"eta/eta_api/models/rag"
+	"eta/eta_api/services/elastic"
+	"eta/eta_api/services/llm"
+	"eta/eta_api/utils"
+	"eta/eta_api/utils/llm/eta_llm/eta_llm_http"
+	"fmt"
+	html2 "golang.org/x/net/html"
+	"html"
+	"os"
+	"path"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// AddWechatPlatform
+// @Description: 添加新的公众号
+// @param item
+func AddWechatPlatform(item *rag.WechatPlatform) {
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("公众号入库后查找最新记录失败,err:%v", err)
+		}
+	}()
+	if item.FakeId != `` {
+		return
+	}
+
+	if item.ArticleLink == `` {
+		return
+	}
+
+	articleLink := item.ArticleLink
+
+	articleDetail, err := llm.SearchByWechatArticle(item.ArticleLink)
+	if err != nil {
+		return
+	}
+
+	if articleDetail.Appuin == `` {
+		err = fmt.Errorf("文章内未匹配到公众号唯一标识")
+		return
+	}
+
+	wechatPlatform := new(rag.WechatPlatform)
+	// 查找是否存在这个公众号id的
+	wechatPlatformInfo, tmpErr := wechatPlatform.GetByFakeID(articleDetail.Appuin)
+	if tmpErr != nil && !utils.IsErrNoRow(tmpErr) {
+		err = tmpErr
+		return
+	}
+	if tmpErr == nil {
+		// 如果找到了,那么需要将当前的给移除掉
+		err = item.Del()
+		if err != nil {
+			return
+		}
+
+		// 并将查出来的微信公众号摘出来的数据重新赋值
+		item = wechatPlatformInfo
+
+	} else if utils.IsErrNoRow(tmpErr) {
+		// 如果没找到,那么就变更当前的信息
+		item.FakeId = articleDetail.Appuin
+		item.Nickname = articleDetail.Nickname
+		//item.Alias = req.Alias
+		item.RoundHeadImg = articleDetail.RoundHeadImg
+		//item.ServiceType = req.ServiceType
+		item.Signature = articleDetail.ProfileSignature
+		//item.Verified = verified
+		item.ModifyTime = time.Now()
+
+		err = item.Update([]string{rag.WechatPlatformColumns.FakeID, rag.WechatPlatformColumns.Nickname, rag.WechatPlatformColumns.RoundHeadImg, rag.WechatPlatformColumns.Signature, rag.WechatPlatformColumns.ModifyTime})
+		if err != nil {
+			return
+		}
+
+		// 修改公众号头像
+		go replaceWechatPlatformPic(item)
+	}
+
+	// 把刚搜索的文章加入到文章库中
+	AddWechatArticle(item, articleLink, articleDetail, nil)
+
+	BeachAddWechatArticle(item, 10)
+	fmt.Println("公众号入库完成")
+
+	return
+}
+
+// AddWechatArticle
+// @Description: 添加公众号文章入库
+// @author: Roc
+// @datetime 2025-03-05 13:24:14
+// @param item *rag.WechatPlatform
+// @param link string
+// @param articleDetail WechatArticleDataResp
+func AddWechatArticle(item *rag.WechatPlatform, articleLink string, articleDetail llm.WechatArticleDataResp, articleMenu *llm.ArticleMenu) {
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("公众号文章入库失败,文章链接:%s ,err:%v", articleLink, err)
+		}
+	}()
+	obj := new(rag.WechatArticle)
+
+	_, err = obj.GetByLink(articleLink)
+	if err == nil {
+		// 文章已经入库了,不需要重复入库
+		return
+	}
+
+	// 如果不是 ErrNoRow 的时候,那么就是查询数据库出问题了,需要直接返回
+	if !utils.IsErrNoRow(err) {
+		return
+	}
+
+	// 这个时候,说明数据库中没有这个文章,那么需要文章入库
+	err = nil
+
+	var publishAt time.Time
+	if articleDetail.CreateAt != `` {
+		createAtInt, tmpErr := strconv.Atoi(articleDetail.CreateAt)
+		if tmpErr == nil {
+			publishAt = time.Unix(int64(createAtInt), 1000)
+		}
+	} else if articleMenu != nil {
+		publishAt = time.Unix(int64(articleMenu.UpdateTime), 1000)
+	}
+
+	content := articleDetail.HtmlContent
+	// 图片下载下来到本地,如果成功了,那么就用新的
+	tmpContent, err := ReplaceHtmlImg(content)
+	if tmpContent != `` {
+		content = tmpContent
+	}
+
+	obj = &rag.WechatArticle{
+		WechatArticleId:  0,
+		WechatPlatformId: item.WechatPlatformId,
+		FakeId:           item.FakeId,
+		Title:            articleDetail.Title,
+		Link:             articleLink,
+		CoverUrl:         articleDetail.CoverUrl,
+		Description:      articleDetail.Desc,
+		Content:          html.EscapeString(content),
+		TextContent:      articleDetail.TextContent,
+		Country:          articleDetail.CountryName,
+		Province:         articleDetail.ProvinceName,
+		City:             articleDetail.CityName,
+		//Abstract:          "",
+		//ArticleCreateTime: createAt,
+		ModifyTime: time.Now(),
+		CreateTime: time.Now(),
+	}
+	if !publishAt.IsZero() {
+		obj.ArticleCreateTime = publishAt
+	}
+
+	if articleMenu != nil {
+		obj.Title = articleMenu.Title
+		//obj.Link = articleMenu.Link
+		obj.CoverUrl = articleMenu.Cover
+		obj.Description = articleMenu.Digest
+	}
+	err = obj.Create()
+
+	// 修改文章封面图
+	go replaceWechatArticleCoverPic(obj)
+
+	// 文章入库成功后,需要将相关信息入摘要库
+	go cache.AddWechatArticleLlmOpToCache(obj.WechatArticleId, ``)
+
+}
+
+// BeachAddWechatArticle
+// @Description: 批量添加公众号文章
+// @param item
+// @param num
+// @return err
+func BeachAddWechatArticle(item *rag.WechatPlatform, num int) {
+	var err error
+	defer func() {
+		//fmt.Println("公众号文章批量入库完成")
+		if err != nil {
+			utils.FileLog.Error("公众号文章批量入库失败,err:%v", err)
+			fmt.Println("公众号文章批量入库失败,err:", err)
+		}
+	}()
+	if item.FakeId == `` {
+		return
+	}
+
+	wechatArticleObj := new(rag.WechatArticle)
+
+	// 获取公众号的文章列表
+	articleListResp, err := llm.SearchByWechatArticleList(item.FakeId, num)
+	if err != nil {
+		return
+	}
+	for _, articleMenu := range articleListResp.List {
+		// 判断文章是否已经入库,如果已经入库了,那么就过滤,不去重复查询微信了
+		_, err = wechatArticleObj.GetByLink(articleMenu.Link)
+		if err == nil {
+			// 文章已经入库了,不需要重复入库
+			continue
+		}
+		if !utils.IsErrNoRow(err) {
+			return
+		}
+		err = nil
+
+		articleDetail, tmpErr := llm.SearchByWechatArticle(articleMenu.Link)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+
+		// 把刚搜索的文章加入到指标库
+		AddWechatArticle(item, articleMenu.Link, articleDetail, &articleMenu)
+
+		time.Sleep(10 * time.Second)
+
+	}
+	return
+}
+
+// GenerateArticleAbstract
+// @Description: 文章摘要生成
+// @author: Roc
+// @datetime 2025-03-10 16:17:53
+// @param item *rag.WechatArticle
+func GenerateArticleAbstract(item *rag.WechatArticle) {
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("文章转临时文件失败,err:%v", err)
+			fmt.Println("文章转临时文件失败,err:", err)
+		}
+	}()
+
+	// 内容为空,那就不需要生成摘要
+	if item.TextContent == `` {
+		return
+	}
+
+	abstractObj := rag.WechatArticleAbstract{}
+	tmpAbstractItem, err := abstractObj.GetByWechatArticleId(item.WechatArticleId)
+	if err == nil {
+		// 摘要已经生成,不需要重复生成
+		AbstractToKnowledge(item, tmpAbstractItem, false)
+
+		return
+	}
+	if !utils.IsErrNoRow(err) {
+		return
+	}
+
+	// 生成临时文件
+	dateDir := time.Now().Format("20060102")
+	uploadDir := utils.STATIC_DIR + "ai/" + dateDir
+	err = os.MkdirAll(uploadDir, utils.DIR_MOD)
+	if err != nil {
+		err = fmt.Errorf("存储目录创建失败,Err:" + err.Error())
+		return
+	}
+	randStr := utils.GetRandStringNoSpecialChar(28)
+	fileName := randStr + `.md`
+	tmpFilePath := uploadDir + "/" + fileName
+	err = utils.SaveToFile(item.TextContent, tmpFilePath)
+	if err != nil {
+		err = fmt.Errorf("生成临时文件失败,Err:" + err.Error())
+		return
+	}
+	defer func() {
+		os.Remove(tmpFilePath)
+	}()
+
+	// 上传临时文件到LLM
+	tmpFileResp, err := llm.UploadTempDocs(tmpFilePath)
+	if err != nil {
+		err = fmt.Errorf("上传临时文件到LLM失败,Err:" + err.Error())
+		return
+	}
+
+	if tmpFileResp.Data.Id == `` {
+		err = fmt.Errorf("上传临时文件到LLM失败,Err:上传失败")
+		return
+	}
+	tmpDocId := tmpFileResp.Data.Id
+
+	//tmpDocId := `c4d2ee902808408c8b8ed398b33be103` // 钢材
+	//tmpDocId := `2dde8afe62d24525a814e74e0a5e35e4` // 钢材
+	//tmpDocId := `7634cc1086c04b3687682220a2cf1a48` //
+
+	//开始对话
+	abstract, addArticleChatRecordList, tmpErr := getAnswerByContent(item.WechatArticleId, tmpDocId)
+	if tmpErr != nil {
+		err = fmt.Errorf("LLM对话失败,Err:" + tmpErr.Error())
+		return
+	}
+
+	// 添加问答记录
+	if len(addArticleChatRecordList) > 0 {
+		recordObj := rag.WechatArticleChatRecord{}
+		err = recordObj.CreateInBatches(addArticleChatRecordList)
+		if err != nil {
+			return
+		}
+	}
+
+	if abstract != `` {
+		if abstract == `sorry` || strings.Index(abstract, `根据已知信息无法回答该问题`) == 0 {
+			item.AbstractStatus = 2
+			item.ModifyTime = time.Now()
+			err = item.Update([]string{"AbstractStatus", "ModifyTime"})
+			return
+		}
+		item.AbstractStatus = 1
+		item.ModifyTime = time.Now()
+		err = item.Update([]string{"AbstractStatus", "ModifyTime"})
+
+		abstractItem := &rag.WechatArticleAbstract{
+			WechatArticleAbstractId: 0,
+			WechatArticleId:         item.WechatArticleId,
+			Content:                 abstract,
+			Version:                 0,
+			VectorKey:               "",
+			ModifyTime:              time.Now(),
+			CreateTime:              time.Now(),
+		}
+		err = abstractItem.Create()
+		if err != nil {
+			return
+		}
+
+		// 数据入ES库
+		go AddOrEditEsWechatArticleAbstract(abstractItem.WechatArticleAbstractId)
+
+		AbstractToKnowledge(item, abstractItem, false)
+	}
+}
+
+// ReGenerateArticleAbstract
+// @Description: 文章摘要重新生成
+// @author: Roc
+// @datetime 2025-03-10 16:17:53
+// @param item *rag.WechatArticle
+func ReGenerateArticleAbstract(item *rag.WechatArticle) {
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("文章转临时文件失败,err:%v", err)
+			fmt.Println("文章转临时文件失败,err:", err)
+		}
+	}()
+
+	abstractObj := rag.WechatArticleAbstract{}
+	abstractItem, err := abstractObj.GetByWechatArticleId(item.WechatArticleId)
+	if err != nil {
+		if utils.IsErrNoRow(err) {
+			// 直接生成
+			GenerateArticleAbstract(item)
+			return
+		}
+		// 异常了
+		return
+	}
+
+	// 生成临时文件
+	dateDir := time.Now().Format("20060102")
+	uploadDir := utils.STATIC_DIR + "ai/" + dateDir
+	err = os.MkdirAll(uploadDir, utils.DIR_MOD)
+	if err != nil {
+		err = fmt.Errorf("存储目录创建失败,Err:" + err.Error())
+		return
+	}
+	randStr := utils.GetRandStringNoSpecialChar(28)
+	fileName := randStr + `.md`
+	tmpFilePath := uploadDir + "/" + fileName
+	err = utils.SaveToFile(item.TextContent, tmpFilePath)
+	if err != nil {
+		err = fmt.Errorf("生成临时文件失败,Err:" + err.Error())
+		return
+	}
+	defer func() {
+		os.Remove(tmpFilePath)
+	}()
+
+	// 上传临时文件到LLM
+	tmpFileResp, err := llm.UploadTempDocs(tmpFilePath)
+	if err != nil {
+		err = fmt.Errorf("上传临时文件到LLM失败,Err:" + err.Error())
+		return
+	}
+
+	if tmpFileResp.Data.Id == `` {
+		err = fmt.Errorf("上传临时文件到LLM失败,Err:上传失败")
+		return
+	}
+	tmpDocId := tmpFileResp.Data.Id
+
+	//tmpDocId := `c4d2ee902808408c8b8ed398b33be103` // 钢材
+	//tmpDocId := `2dde8afe62d24525a814e74e0a5e35e4` // 钢材
+	//tmpDocId := `7634cc1086c04b3687682220a2cf1a48` //
+
+	//开始对话
+	abstract, addArticleChatRecordList, tmpErr := getAnswerByContent(item.WechatArticleId, tmpDocId)
+	if tmpErr != nil {
+		err = fmt.Errorf("LLM对话失败,Err:" + tmpErr.Error())
+		return
+	}
+
+	// 添加问答记录
+	if len(addArticleChatRecordList) > 0 {
+		recordObj := rag.WechatArticleChatRecord{}
+		err = recordObj.CreateInBatches(addArticleChatRecordList)
+		if err != nil {
+			return
+		}
+	}
+
+	if abstract != `` {
+		if abstract == `sorry` || strings.Index(abstract, `根据已知信息无法回答该问题`) == 0 {
+			item.AbstractStatus = 2
+			item.ModifyTime = time.Now()
+			err = item.Update([]string{"AbstractStatus", "ModifyTime"})
+			return
+		}
+		item.AbstractStatus = 1
+		item.ModifyTime = time.Now()
+		err = item.Update([]string{"AbstractStatus", "ModifyTime"})
+
+		abstractItem.Content = abstract
+		abstractItem.Version = abstractObj.Version + 1
+		abstractItem.ModifyTime = time.Now()
+		err = abstractItem.Update([]string{"content", "version", "modify_time"})
+		if err != nil {
+			return
+		}
+
+		AbstractToKnowledge(item, abstractItem, true)
+	}
+}
+
+// DelDoc
+// @Description: 删除摘要向量库
+// @author: Roc
+// @datetime 2025-03-12 16:55:05
+// @param wechatArticleAbstractList []*rag.WechatArticleAbstract
+// @return err error
+func DelDoc(wechatArticleAbstractList []*rag.WechatArticleAbstract) (err error) {
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("删除摘要向量库文件失败,err:%v", err)
+			fmt.Println("删除摘要向量库文件失败,err:", err)
+		}
+	}()
+
+	vectorKeyList := make([]string, 0)
+	wechatArticleAbstractIdList := make([]int, 0)
+
+	for _, v := range wechatArticleAbstractList {
+		if v.VectorKey == `` {
+			continue
+		}
+		vectorKeyList = append(vectorKeyList, v.VectorKey)
+		wechatArticleAbstractIdList = append(wechatArticleAbstractIdList, v.WechatArticleAbstractId)
+	}
+
+	// 没有就不删除
+	if len(vectorKeyList) <= 0 {
+		return
+	}
+
+	_, err = llm.DelDocsToKnowledge(models.BusinessConfMap[models.KnowledgeBaseName], vectorKeyList)
+	if err != nil {
+		err = fmt.Errorf("删除LLM摘要向量库文件失败,Err:" + err.Error())
+		return
+	}
+	//fmt.Println(resp)
+	obj := rag.WechatArticleAbstract{}
+	err = obj.DelVectorKey(wechatArticleAbstractIdList)
+
+	return
+}
+
+// DelLlmDoc
+// @Description: 删除摘要向量库
+// @author: Roc
+// @datetime 2025-03-12 16:55:05
+// @param wechatArticleAbstractList []*rag.WechatArticleAbstract
+// @return err error
+func DelLlmDoc(vectorKeyList []string, wechatArticleAbstractIdList []int) (err error) {
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("删除摘要向量库文件失败,err:%v", err)
+			fmt.Println("删除摘要向量库文件失败,err:", err)
+		}
+	}()
+
+	// 没有就不删除
+	if len(vectorKeyList) <= 0 {
+		return
+	}
+
+	_, err = llm.DelDocsToKnowledge(models.BusinessConfMap[models.KnowledgeBaseName], vectorKeyList)
+	if err != nil {
+		err = fmt.Errorf("删除LLM摘要向量库文件失败,Err:" + err.Error())
+		return
+	}
+	//fmt.Println(resp)
+	obj := rag.WechatArticleAbstract{}
+	err = obj.DelVectorKey(wechatArticleAbstractIdList)
+
+	return
+}
+
+func getAnswerByContent(wechatArticleId int, docId string) (answer string, addArticleChatRecordList []*rag.WechatArticleChatRecord, err error) {
+	historyList := make([]eta_llm_http.HistoryContent, 0)
+	addArticleChatRecordList = make([]*rag.WechatArticleChatRecord, 0)
+
+	questionObj := rag.Question{}
+	questionList, err := questionObj.GetListByCondition(``, []interface{}{}, 0, 100)
+	if err != nil {
+		err = fmt.Errorf("获取问题列表失败,Err:" + err.Error())
+		return
+	}
+
+	// 没问题就不生成了
+	if len(questionList) <= 0 {
+		return
+	}
+
+	//你现在是一名资深的期货行业分析师,请基于以下的问题进行汇总总结,如果不能正常总结出来,那么就只需要回复我:sorry
+	questionStrList := []string{`你现在是一名资深的期货行业分析师,请基于以下的问题进行汇总总结,如果不能正常总结出来,那么就只需要回复我:sorry。以下是问题:`}
+	for _, v := range questionList {
+		questionStrList = append(questionStrList, v.QuestionContent)
+	}
+	questionStr := strings.Join(questionStrList, "\n")
+
+	originalAnswer, result, err := llm.ChatByFile(docId, questionStr, historyList)
+	fmt.Println(result)
+	if err != nil {
+		err = fmt.Errorf("LLM对话失败,Err:" + err.Error())
+		return
+	}
+
+	// 提取 </think> 后面的内容
+	thinkEndIndex := strings.Index(result.Answer, "</think>")
+	if thinkEndIndex != -1 {
+		answer = strings.TrimSpace(result.Answer[thinkEndIndex+len("</think>"):])
+	} else {
+		answer = result.Answer
+	}
+
+	answer = strings.TrimSpace(answer)
+
+	// 待入库的数据
+	addArticleChatRecordList = append(addArticleChatRecordList, &rag.WechatArticleChatRecord{
+		WechatArticleChatRecordId: 0,
+		WechatArticleId:           wechatArticleId,
+		ChatUserType:              "user",
+		Content:                   questionStr,
+		SendTime:                  time.Now(),
+		CreatedTime:               time.Now(),
+		UpdateTime:                time.Now(),
+	}, &rag.WechatArticleChatRecord{
+		WechatArticleChatRecordId: 0,
+		WechatArticleId:           wechatArticleId,
+		ChatUserType:              "assistant",
+		Content:                   originalAnswer,
+		SendTime:                  time.Now(),
+		CreatedTime:               time.Now(),
+		UpdateTime:                time.Now(),
+	})
+
+	return
+}
+
+// ArticleToKnowledge
+// @Description: 原文入向量库
+// @author: Roc
+// @datetime 2025-03-10 16:13:16
+// @param item *rag.WechatArticle
+func ArticleToKnowledge(item *rag.WechatArticle) {
+	if item.TextContent == `` {
+		return
+	}
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("上传文章原文到知识库失败,err:%v", err)
+			fmt.Println("上传文章原文到知识库失败,err:", err)
+		}
+	}()
+
+	// 生成临时文件
+	//dateDir := time.Now().Format("20060102")
+	//uploadDir := utils.STATIC_DIR + "ai/article/" + dateDir
+	uploadDir := utils.STATIC_DIR + "ai/article"
+	err = os.MkdirAll(uploadDir, utils.DIR_MOD)
+	if err != nil {
+		err = fmt.Errorf("存储目录创建失败,Err:" + err.Error())
+		return
+	}
+	fileName := utils.RemoveSpecialChars(item.Title) + `.md`
+	tmpFilePath := uploadDir + "/" + fileName
+	err = utils.SaveToFile(item.TextContent, tmpFilePath)
+	if err != nil {
+		err = fmt.Errorf("生成临时文件失败,Err:" + err.Error())
+		return
+	}
+	defer func() {
+		os.Remove(tmpFilePath)
+	}()
+
+	knowledgeArticleName := models.BusinessConfMap[models.KnowledgeArticleName]
+	// 上传临时文件到LLM
+	uploadFileResp, err := llm.UploadDocsToKnowledge(tmpFilePath, knowledgeArticleName)
+	if err != nil {
+		err = fmt.Errorf("上传文章原文到知识库失败,Err:" + err.Error())
+		return
+	}
+
+	if len(uploadFileResp.FailedFiles) > 0 {
+		for _, v := range uploadFileResp.FailedFiles {
+			err = fmt.Errorf("上传文章原文到知识库失败,Err:" + v)
+		}
+	}
+
+	item.VectorKey = tmpFilePath
+	item.ModifyTime = time.Now()
+	err = item.Update([]string{"vector_key", "modify_time"})
+
+}
+
+// AbstractToKnowledge
+// @Description: 摘要入向量库
+// @author: Roc
+// @datetime 2025-03-10 16:14:59
+// @param wechatArticleItem *rag.WechatArticle
+// @param abstractItem *rag.WechatArticleAbstract
+func AbstractToKnowledge(wechatArticleItem *rag.WechatArticle, abstractItem *rag.WechatArticleAbstract, isReUpload bool) {
+	if abstractItem.Content == `` {
+		return
+	}
+	// 已经生成了,那就不处理了
+	if abstractItem.VectorKey != `` && !isReUpload {
+		return
+	}
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("摘要入向量库失败,err:%v", err)
+			fmt.Println("摘要入向量库失败,err:", err)
+		}
+
+		// 数据入ES库
+		go AddOrEditEsWechatArticleAbstract(abstractItem.WechatArticleAbstractId)
+	}()
+
+	// 生成临时文件
+	//dateDir := time.Now().Format("20060102")
+	//uploadDir := utils.STATIC_DIR + "ai/article/" + dateDir
+	uploadDir := utils.STATIC_DIR + "ai/abstract"
+	err = os.MkdirAll(uploadDir, utils.DIR_MOD)
+	if err != nil {
+		err = fmt.Errorf("存储目录创建失败,Err:" + err.Error())
+		return
+	}
+	fileName := utils.RemoveSpecialChars(wechatArticleItem.Title) + `.md`
+	tmpFilePath := uploadDir + "/" + fileName
+	err = utils.SaveToFile(abstractItem.Content, tmpFilePath)
+	if err != nil {
+		err = fmt.Errorf("生成临时文件失败,Err:" + err.Error())
+		return
+	}
+	defer func() {
+		os.Remove(tmpFilePath)
+	}()
+
+	knowledgeArticleName := models.BusinessConfMap[models.KnowledgeBaseName]
+	// 上传临时文件到LLM
+	uploadFileResp, err := llm.UploadDocsToKnowledge(tmpFilePath, knowledgeArticleName)
+	if err != nil {
+		err = fmt.Errorf("上传文章原文到知识库失败,Err:" + err.Error())
+		return
+	}
+
+	if len(uploadFileResp.FailedFiles) > 0 {
+		for _, v := range uploadFileResp.FailedFiles {
+			err = fmt.Errorf("上传文章原文到知识库失败,Err:" + v)
+		}
+	}
+
+	abstractItem.VectorKey = tmpFilePath
+	abstractItem.ModifyTime = time.Now()
+	err = abstractItem.Update([]string{"vector_key", "modify_time"})
+
+}
+
+// replaceWechatPlatformPic
+// @Description: 替换公众号头像
+// @author: Roc
+// @datetime 2025-03-11 09:38:24
+// @param item *rag.WechatPlatform
+func replaceWechatPlatformPic(item *rag.WechatPlatform) {
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("替换公众号头像失败,err:%v", err)
+			fmt.Println("替换公众号头像失败,err:", err)
+		}
+	}()
+	if item.RoundHeadImg == `` {
+		return
+	}
+	resourceUrl, err := downloadWxPicAndUploadToOss(item.RoundHeadImg, `head_img`)
+	if err != nil {
+		return
+	}
+	item.RoundHeadImg = resourceUrl
+	err = item.Update([]string{"round_head_img"})
+
+}
+
+// replaceWechatArticleCoverPic
+// @Description: 替换文章封面图
+// @author: Roc
+// @datetime 2025-03-11 09:38:35
+// @param item *rag.WechatArticle
+func replaceWechatArticleCoverPic(item *rag.WechatArticle) {
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("替换公众号头像失败,err:%v", err)
+			fmt.Println("替换公众号头像失败,err:", err)
+		}
+
+		// 数据入ES库
+		AddOrEditEsWechatArticle(item.WechatArticleId)
+	}()
+	if item.CoverUrl == `` {
+		return
+	}
+	resourceUrl, err := downloadWxPicAndUploadToOss(item.CoverUrl, `cover_url`)
+	if err != nil {
+		return
+	}
+	item.CoverUrl = resourceUrl
+	err = item.Update([]string{"cover_url"})
+
+}
+
+// replaceWechatArticlePic
+// @Description: 替换文章内容图
+// @author: Roc
+// @datetime 2025-03-11 09:38:35
+// @param item *rag.WechatArticle
+func ReplaceWechatArticlePic(item *rag.WechatArticle) {
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("替换公众号头像失败,err:%v", err)
+			fmt.Println("替换公众号头像失败,err:", err)
+		}
+	}()
+	if item.Content == `` {
+		return
+	}
+
+	content, err := ReplaceHtmlImg(html.UnescapeString(item.Content))
+	if err != nil {
+		return
+	}
+	item.Content = html.EscapeString(content)
+	err = item.Update([]string{"content"})
+
+	return
+}
+
+// downloadWxPicAndUploadToOss
+// @Description: 下载微信图片并上传到OSS
+// @author: Roc
+// @datetime 2025-03-11 09:28:49
+// @param wxPicUrl string
+// @return resourceUrl string
+// @return err error
+func downloadWxPicAndUploadToOss(wxPicUrl, source string) (resourceUrl string, err error) {
+	localFilePath, err := utils.DownloadWxImage(wxPicUrl)
+	if err != nil {
+		return
+	}
+	defer func() {
+		os.Remove(localFilePath)
+	}()
+	ossClient := NewOssClient()
+	if ossClient == nil {
+		err = fmt.Errorf(`初始化OSS服务失败`)
+		return
+	}
+	ext := path.Ext(localFilePath)
+	fileName := fmt.Sprintf(`%s%s%s`, time.Now().Format(utils.FormatShortDateTimeUnSpace), utils.GetRandStringNoSpecialChar(16), ext)
+	//savePath := utils.UploadDir + `wx/wx_article/` + time.Now().Format("200601/20060102/") + fileName
+	savePath := fmt.Sprintf(`%swx/%s/%s%s`, utils.UploadDir, source, time.Now().Format("200601/20060102/"), fileName)
+	resourceUrl, err = ossClient.UploadFile(fileName, localFilePath, savePath)
+	if err != nil {
+		err = fmt.Errorf("文件上传失败,Err:" + err.Error())
+		return
+	}
+
+	return
+
+}
+
+// ReplaceHtmlImg
+// @Description: 将html中的图片替换成自己的
+// @author: Roc
+// @datetime 2025-03-11 14:32:00
+// @param htmlStr string
+// @return newHtml string
+// @return err error
+func ReplaceHtmlImg(htmlStr string) (newHtml string, err error) {
+	doc, err := html2.Parse(strings.NewReader(htmlStr))
+	if err != nil {
+		return
+	}
+	if err != nil {
+		return
+	}
+	handleNode(doc)
+
+	// 将处理后的HTML节点重新渲染为HTML字符串
+	var buf bytes.Buffer
+	if err = html2.Render(&buf, doc); err != nil {
+		fmt.Println(err)
+		return
+	}
+	newHtml = buf.String()
+
+	return
+}
+
+// handleNode
+// @Description: html节点处理
+// @author: Roc
+// @datetime 2025-03-11 14:32:45
+// @param n *html2.Node
+func handleNode(n *html2.Node) {
+	if n.Type == html2.ElementNode {
+		if n.Data == "img" {
+			for k, attr := range n.Attr {
+				// 新增代码:如果标签是img且存在data-src属性,则将data-src的值赋给src
+				if n.Data == "img" && attr.Key == "src" {
+					resourceUrl, tmpErr := downloadWxPicAndUploadToOss(attr.Val, `article`)
+					if tmpErr != nil {
+						continue
+					}
+					attr.Val = resourceUrl
+				}
+				n.Attr[k] = attr
+			}
+		}
+
+	}
+	for c := n.FirstChild; c != nil; c = c.NextSibling {
+		handleNode(c)
+	}
+}
+
+// AddOrEditEsWechatPlatformId
+// @Description: 批量处理某个公众号下的文章到ES
+// @author: Roc
+// @datetime 2025-03-13 11:01:28
+// @param articleId int
+func AddOrEditEsWechatPlatformId(wechatPlatformId int) {
+	if utils.EsWechatArticleName == `` {
+		return
+	}
+	obj := rag.WechatArticle{}
+	list, _ := obj.GetListByCondition(` wechat_article_id `, ` AND wechat_platform_id = ? `, []interface{}{wechatPlatformId}, 0, 1000000)
+	for _, item := range list {
+		AddOrEditEsWechatArticle(item.WechatArticleId)
+	}
+}
+
+// AddOrEditEsWechatArticle
+// @Description: 新增/编辑微信文章入ES
+// @author: Roc
+// @datetime 2025-03-13 11:01:28
+// @param articleId int
+func AddOrEditEsWechatArticle(articleId int) {
+	if utils.EsWechatArticleName == `` {
+		return
+	}
+
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("添加公众号微信信息到ES失败,err:%v", err)
+			fmt.Println("添加公众号微信信息到ES失败,err:", err)
+		}
+	}()
+	obj := rag.WechatArticle{}
+	articleInfo, err := obj.GetById(articleId)
+	if err != nil {
+		err = fmt.Errorf("获取公众号文章信息失败,Err:" + err.Error())
+		return
+	}
+	platformObj := rag.WechatPlatform{}
+	platformInfo, err := platformObj.GetById(articleInfo.WechatPlatformId)
+	if err != nil {
+		err = fmt.Errorf("获取公众号平台信息失败,Err:" + err.Error())
+		return
+	}
+
+	esItem := elastic.WechatArticleAndPlatform{
+		WechatArticleId:  articleInfo.WechatArticleId,
+		WechatPlatformId: articleInfo.WechatPlatformId,
+		FakeId:           articleInfo.FakeId,
+		Title:            articleInfo.Title,
+		Link:             articleInfo.Link,
+		CoverUrl:         articleInfo.CoverUrl,
+		Description:      articleInfo.Description,
+		//Content:          articleInfo.Content,
+		//TextContent: articleInfo.TextContent,
+		//AbstractStatus:          articleInfo.AbstractStatus,
+		Country:           articleInfo.Country,
+		Province:          articleInfo.Province,
+		City:              articleInfo.City,
+		ArticleCreateTime: articleInfo.ArticleCreateTime,
+		IsDeleted:         articleInfo.IsDeleted,
+		ModifyTime:        articleInfo.ModifyTime,
+		CreateTime:        articleInfo.CreateTime,
+		Nickname:          platformInfo.Nickname,
+		Alias:             platformInfo.Alias,
+		RoundHeadImg:      platformInfo.RoundHeadImg,
+	}
+
+	err = elastic.WechatArticleEsAddOrEdit(strconv.Itoa(articleInfo.WechatArticleId), esItem)
+}
+
+// AddOrEditEsWechatArticleAbstract
+// @Description: 新增/编辑微信文章摘要入ES
+// @author: Roc
+// @datetime 2025-03-13 14:13:47
+// @param articleAbstractId int
+func AddOrEditEsWechatArticleAbstract(articleAbstractId int) {
+	if utils.EsWechatArticleAbstractName == `` {
+		return
+	}
+
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("添加公众号微信信息到ES失败,err:%v", err)
+			fmt.Println("添加公众号微信信息到ES失败,err:", err)
+		}
+	}()
+	obj := rag.WechatArticleAbstract{}
+	abstractInfo, err := obj.GetById(articleAbstractId)
+	if err != nil {
+		err = fmt.Errorf("获取公众号文章信息失败,Err:" + err.Error())
+		return
+	}
+	articleObj := rag.WechatArticle{}
+	articleInfo, err := articleObj.GetById(abstractInfo.WechatArticleId)
+	if err != nil {
+		err = fmt.Errorf("获取公众号文章信息失败,Err:" + err.Error())
+		return
+	}
+
+	// 公众号平台关联的标签品种
+	tagObj := rag.WechatPlatformTagMapping{}
+	tagMappingList, err := tagObj.GetListByCondition(` AND wechat_platform_id = ? `, []interface{}{articleInfo.WechatPlatformId}, 0, 10000)
+	if err != nil {
+		err = fmt.Errorf("获取公众号平台关联的品种信息失败,Err:" + err.Error())
+		return
+	}
+
+	tagIdList := make([]int, 0)
+	for _, v := range tagMappingList {
+		tagIdList = append(tagIdList, v.TagId)
+	}
+
+	esItem := elastic.WechatArticleAbstractItem{
+		WechatArticleAbstractId: abstractInfo.WechatArticleAbstractId,
+		WechatArticleId:         abstractInfo.WechatArticleId,
+		WechatPlatformId:        articleInfo.WechatPlatformId,
+		Abstract:                abstractInfo.Content,
+		Version:                 abstractInfo.Version,
+		VectorKey:               abstractInfo.VectorKey,
+		ModifyTime:              articleInfo.ModifyTime,
+		CreateTime:              articleInfo.CreateTime,
+		Title:                   articleInfo.Title,
+		Link:                    articleInfo.Link,
+		TagIdList:               tagIdList,
+	}
+
+	err = elastic.WechatArticleAbstractEsAddOrEdit(strconv.Itoa(articleAbstractId), esItem)
+}
+
+// AddOrEditEsWechatArticleAbstract
+// @Description: 新增/编辑微信文章摘要入ES
+// @author: Roc
+// @datetime 2025-03-13 14:13:47
+// @param articleAbstractId int
+func DelEsWechatArticleAbstract(articleAbstractId int) {
+	if utils.EsWechatArticleAbstractName == `` {
+		return
+	}
+
+	var err error
+	defer func() {
+		if err != nil {
+			utils.FileLog.Error("添加公众号微信信息到ES失败,err:%v", err)
+			fmt.Println("添加公众号微信信息到ES失败,err:", err)
+		}
+	}()
+
+	err = elastic.WechatArticleAbstractEsDel(strconv.Itoa(articleAbstractId))
+}

+ 134 - 0
services/ws_service.go

@@ -0,0 +1,134 @@
+package services
+
+import (
+	"eta/eta_api/utils/ws"
+)
+
+var ()
+
+//func WsAuthenticate() web.FilterFunc {
+//	return func(ctx *context.Context) {
+//		method := ctx.Input.Method()
+//		uri := ctx.Input.URI()
+//		if method == "GET" {
+//			authorization := ctx.Input.Header("authorization")
+//			if authorization == "" {
+//				authorization = ctx.Input.Header("Authorization")
+//			}
+//			if strings.Contains(authorization, ";") {
+//				authorization = strings.Replace(authorization, ";", "$", 1)
+//			}
+//			if authorization == "" {
+//				strArr := strings.Split(uri, "?")
+//				for k, v := range strArr {
+//					fmt.Println(k, v)
+//				}
+//				if len(strArr) > 1 {
+//					authorization = strArr[1]
+//					authorization = strings.Replace(authorization, "Authorization", "authorization", -1)
+//				}
+//			}
+//			if authorization == "" {
+//				utils.FileLog.Error("authorization为空,未授权")
+//				ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
+//				return
+//			}
+//			tokenStr := authorization
+//			tokenArr := strings.Split(tokenStr, "=")
+//			token := tokenArr[1]
+//
+//			session, err := system.GetSysSessionByToken(token)
+//			if err != nil {
+//				if utils.IsErrNoRow(err) {
+//					utils.FileLog.Error("authorization已过期")
+//					ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
+//					return
+//				}
+//				utils.FileLog.Error("authorization查询用户信息失败")
+//				ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
+//				return
+//			}
+//			if session == nil {
+//				utils.FileLog.Error("会话不存在")
+//				ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
+//				return
+//			}
+//			//校验token是否合法
+//			// JWT校验Token和Account
+//			account := utils.MD5(session.UserName)
+//			if !utils.CheckToken(account, token) {
+//				utils.FileLog.Error("authorization校验不合法")
+//				ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
+//				return
+//			}
+//			if time.Now().After(session.ExpiredTime) {
+//				utils.FileLog.Error("authorization过期法")
+//				ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
+//				return
+//			}
+//			admin, err := system.GetSysUserById(session.SysUserId)
+//			if err != nil {
+//				if utils.IsErrNoRow(err) {
+//					utils.FileLog.Error("权限不够")
+//					ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+//					return
+//				}
+//				utils.FileLog.Error("获取用户信息失败")
+//				ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
+//				return
+//			}
+//			if admin == nil {
+//				utils.FileLog.Error("权限不够")
+//				ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+//				return
+//			}
+//			//如果不是启用状态
+//			if admin.Enabled != 1 {
+//				utils.FileLog.Error("用户被禁用")
+//				ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+//				return
+//			}
+//
+//			//接口权限校验
+//			roleId := admin.RoleId
+//			list, e := system.GetMenuButtonApisByRoleId(roleId)
+//			if e != nil {
+//				utils.FileLog.Error("接口权限查询出错", e)
+//				ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+//				return
+//			}
+//			var api string
+//			for _, v := range list {
+//				if v.Api != "" {
+//					api += v.Api + "&"
+//				}
+//			}
+//			api += "&" + models.BusinessConfMap["PublicApi"]
+//			//处理uri请求,去除前缀和参数
+//			api = strings.TrimRight(api, "&")
+//			uri = strings.Replace(uri, "/adminapi", "", 1)
+//			uris := strings.Split(uri, "?")
+//			uri = uris[0]
+//			//fmt.Println("uri:", uri)
+//			apis := strings.Split(api, "&")
+//			apiMap := make(map[string]bool, 0)
+//			for _, s := range apis {
+//				apiMap[s] = true
+//			}
+//			if !apiMap[uri] {
+//				utils.FileLog.Error("用户无权访问")
+//				ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
+//				return
+//			}
+//			ctx.Input.SetData("admin", admin)
+//		} else {
+//			utils.FileLog.Error("请求方法类型错误")
+//			ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
+//			return
+//		}
+//	}
+//}
+
+func StartSessionManager() {
+	ws.GetInstance().Start()
+}

+ 100 - 0
utils/common.go

@@ -329,6 +329,62 @@ func DownloadImage(imgUrl string) (filePath string, err error) {
 	return
 }
 
+// 下载图片
+func DownloadWxImage(imgUrl string) (filePath string, err error) {
+	imgPath := "./static/imgs/"
+	ext, err := GetFileExtensionFromURL(imgUrl)
+	if err != nil {
+		return
+	}
+	randStr := GetRandStringNoSpecialChar(28)
+	fileName := randStr + `.` + ext
+
+	res, err := http.Get(imgUrl)
+	if err != nil {
+		fmt.Println("A error occurred!")
+		return
+	}
+	defer res.Body.Close()
+	// 获得get请求响应的reader对象
+	reader := bufio.NewReaderSize(res.Body, 32*1024)
+
+	filePath = imgPath + fileName
+	file, err := os.Create(filePath)
+	if err != nil {
+		return
+	}
+	defer func() {
+		file.Close()
+	}()
+	// 获得文件的writer对象
+	writer := bufio.NewWriter(file)
+
+	_, err = io.Copy(writer, reader)
+	//fmt.Printf("Total length: %d \n", written)
+
+	return
+}
+
+// GetFileExtensionFromURL extracts the file extension from a URL query parameter wx_fmt
+func GetFileExtensionFromURL(rawURL string) (string, error) {
+	parsedURL, err := url.Parse(rawURL)
+	if err != nil {
+		return "", err
+	}
+
+	queryParams, err := url.ParseQuery(parsedURL.RawQuery)
+	if err != nil {
+		return "", err
+	}
+
+	wx_fmtValues, exists := queryParams["wx_fmt"]
+	if !exists || len(wx_fmtValues) == 0 {
+		return "", fmt.Errorf("wx_fmt parameter not found in URL")
+	}
+
+	return wx_fmtValues[0], nil
+}
+
 // DownloadFile 下载文件
 func DownloadFile(fileUrl, fileDir string) (filePath string, err error) {
 	filePathDir := "./static/imgs/"
@@ -2950,3 +3006,47 @@ func GormDateStrToDateTimeStr(originalString string) (formatStr string) {
 
 	return
 }
+
+// DateStrToDateStr
+// @Description: 将日期格式转正常显示的日期字符串
+// @author: Roc
+// @datetime 2025-03-07 11:24:19
+// @param date time.Time
+// @return formatStr string
+func DateStrToDateStr(date time.Time) (formatStr string) {
+	if date.IsZero() {
+		return
+	}
+	// 格式化时间
+	formatStr = date.Format(FormatDate)
+
+	return
+}
+
+// DateStrToDateTimeStr
+// @Description: 将日期格式转正常显示的日期时间字符串
+// @author: Roc
+// @datetime 2025-03-07 11:23:09
+// @param date time.Time
+// @return formatStr string
+func DateStrToDateTimeStr(date time.Time) (formatStr string) {
+	if date.IsZero() {
+		return
+	}
+	// 格式化时间
+	formatStr = date.Format(FormatDateTime)
+
+	return
+}
+
+// RemoveSpecialChars
+// @Description: 移除特殊字符
+// @author: Roc
+// @datetime 2025-03-10 21:06:38
+// @param text string
+// @return string
+func RemoveSpecialChars(text string) string {
+	// 匹配非中文、非字母、非数字、非中文标点的字符
+	reg := regexp.MustCompile(`[^\p{Han}\p{L}\p{N}\x{3000}-\x{303F}]`)
+	return reg.ReplaceAllString(text, "")
+}

+ 17 - 0
utils/config.go

@@ -11,6 +11,11 @@ import (
 	"github.com/spf13/viper"
 )
 
+// 大模型配置
+var (
+	LLM_SERVER string //模型服务地址
+	LLM_MODEL  string
+)
 var (
 	RunMode          string //运行模式
 	MYSQL_URL        string //数据库连接
@@ -141,6 +146,8 @@ var (
 	SmartReportIndexName           string //智能研报ES索引
 	EsExcelIndexName               string // 表格ES索引名称
 	EsDataSourceIndexName          string // 数据源ES索引名称
+	EsWechatArticleName            string // ES索引名称-微信文章
+	EsWechatArticleAbstractName    string // ES索引名称-微信文章摘要
 )
 
 var (
@@ -209,6 +216,10 @@ var (
 	ETA_FORUM_HUB_MD5_KEY string
 )
 
+var (
+	ETA_WX_CRAWLER_URL string
+)
+
 // BusinessCode 商家编码
 var BusinessCode string
 
@@ -577,6 +588,12 @@ func init() {
 		ETA_FORUM_HUB_NAME_EN = config["eta_forum_hub_name_en"]
 		ETA_FORUM_HUB_MD5_KEY = config["eta_forum_hub_md5_key"]
 	}
+
+	// 微信爬虫服务
+	{
+		ETA_WX_CRAWLER_URL = config["eta_wx_crawler_url"]
+	}
+
 	// 商家编码
 	BusinessCode = config["business_code"]
 	// eta_mini_bridge 小程序桥接服务地址

+ 3 - 1
utils/constants.go

@@ -260,7 +260,9 @@ const (
 
 	CACHE_DATA_SOURCE_ES_HANDLE = "eta:data_source_es:handle" // 数据源es处理队列
 
-	CACHE_EXCEL_REFRESH = "CACHE_EXCEL_REFRESH" // 表格刷新
+	CACHE_EXCEL_REFRESH                     = "CACHE_EXCEL_REFRESH"                  // 表格刷新
+	CACHE_WECHAT_PLATFORM_ARTICLE           = "wechat_platform:article:op"           //微信文章处理
+	CACHE_WECHAT_PLATFORM_ARTICLE_KNOWLEDGE = "wechat_platform:article:knowledge:op" //微信文章入知识库处理
 )
 
 // 模板消息推送类型

+ 252 - 0
utils/llm/eta_llm/eta_llm_client.go

@@ -0,0 +1,252 @@
+package eta_llm
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/json"
+	"errors"
+	"eta/eta_api/models"
+	"eta/eta_api/utils"
+	"eta/eta_api/utils/llm"
+	"eta/eta_api/utils/llm/eta_llm/eta_llm_http"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"sync"
+)
+
+var (
+	dsOnce sync.Once
+
+	etaLlmClient *ETALLMClient
+)
+
+const (
+	KNOWLEDEG_CHAT_MODE            = "local_kb"
+	DEFALUT_PROMPT_NAME            = "default"
+	CONTENT_TYPE_JSON              = "application/json"
+	KNOWLEDGE_BASE_CHAT_API        = "/chat/kb_chat"
+	DOCUMENT_CHAT_API              = "/chat/file_chat"
+	KNOWLEDGE_BASE_SEARCH_DOCS_API = "/knowledge_base/search_docs"
+)
+
+type ETALLMClient struct {
+	*llm.LLMClient
+	LlmModel string
+}
+
+type LLMConfig struct {
+	LlmAddress string `json:"llm_server"`
+	LlmModel   string `json:"llm_model"`
+}
+
+func GetInstance() llm.LLMService {
+	dsOnce.Do(func() {
+		confStr := models.BusinessConfMap[models.LLMInitConfig]
+		if confStr == "" {
+			utils.FileLog.Error("LLM配置为空")
+			return
+		}
+
+		var config LLMConfig
+		err := json.Unmarshal([]byte(confStr), &config)
+		if err != nil {
+			utils.FileLog.Error("LLM配置错误")
+		}
+		if etaLlmClient == nil {
+			etaLlmClient = &ETALLMClient{
+				LLMClient: llm.NewLLMClient(config.LlmAddress, 120),
+				LlmModel:  config.LlmModel,
+			}
+		}
+	})
+	return etaLlmClient
+}
+
+func (ds *ETALLMClient) DocumentChat(query string, KnowledgeId string, history []json.RawMessage, stream bool) (llmRes *http.Response, err error) {
+	ChatHistory := make([]eta_llm_http.HistoryContent, 0)
+	for _, historyItemStr := range history {
+		var historyItem eta_llm_http.HistoryContent
+		parseErr := json.Unmarshal(historyItemStr, &historyItem)
+		if parseErr != nil {
+			continue
+		}
+		//str := strings.Split(historyItemStr, "-")
+		//historyItem := eta_llm_http.HistoryContent{
+		//	Role:    str[0],
+		//	Content: str[1],
+		//}
+		ChatHistory = append(ChatHistory, historyItem)
+	}
+	kbReq := eta_llm_http.DocumentChatRequest{
+		Query:       query,
+		KnowledgeId: KnowledgeId,
+		History:     ChatHistory,
+		TopK:        3,
+		//ScoreThreshold: 0.5,
+		ScoreThreshold: 2,
+		Stream:         stream,
+		ModelName:      ds.LlmModel,
+		//Temperature:    0.7,
+		Temperature: 0.01,
+		MaxTokens:   0,
+		//PromptName:  DEFALUT_PROMPT_NAME,
+	}
+	//fmt.Printf("%v", kbReq.History)
+	body, err := json.Marshal(kbReq)
+	fmt.Println(string(body))
+	if err != nil {
+		return
+	}
+	return ds.DoStreamPost(DOCUMENT_CHAT_API, body)
+}
+
+func (ds *ETALLMClient) KnowledgeBaseChat(query string, KnowledgeBaseName string, history []json.RawMessage) (llmRes *http.Response, err error) {
+	ChatHistory := make([]eta_llm_http.HistoryContent, 0)
+	for _, historyItemStr := range history {
+		var historyItem eta_llm_http.HistoryContentWeb
+		parseErr := json.Unmarshal(historyItemStr, &historyItem)
+		if parseErr != nil {
+			continue
+		}
+		ChatHistory = append(ChatHistory, eta_llm_http.HistoryContent{
+			Content: historyItem.Content,
+			Role:    historyItem.Role,
+		})
+	}
+	kbReq := eta_llm_http.KbChatRequest{
+		Query:          query,
+		Mode:           KNOWLEDEG_CHAT_MODE,
+		KbName:         KnowledgeBaseName,
+		History:        ChatHistory,
+		TopK:           3,
+		ScoreThreshold: 0.5,
+		Stream:         true,
+		Model:          ds.LlmModel,
+		Temperature:    0.7,
+		MaxTokens:      0,
+		PromptName:     DEFALUT_PROMPT_NAME,
+		ReturnDirect:   false,
+	}
+	fmt.Printf("%v", kbReq.History)
+	body, err := json.Marshal(kbReq)
+	if err != nil {
+		return
+	}
+	return ds.DoStreamPost(KNOWLEDGE_BASE_CHAT_API, body)
+}
+
+func (ds *ETALLMClient) SearchKbDocs(query string, KnowledgeBaseName string) (content interface{}, err error) {
+	kbReq := eta_llm_http.KbSearchDocsRequest{
+		Query:             query,
+		KnowledgeBaseName: KnowledgeBaseName,
+		TopK:              10,
+		ScoreThreshold:    0.5,
+		Metadata:          struct{}{},
+	}
+
+	body, err := json.Marshal(kbReq)
+	if err != nil {
+		return
+	}
+	resp, err := ds.DoPost(KNOWLEDGE_BASE_SEARCH_DOCS_API, body)
+	if !resp.Success {
+		err = errors.New(resp.Msg)
+		return
+	}
+	if resp.Data != nil {
+		var kbSearchRes []eta_llm_http.SearchDocsResponse
+		err = json.Unmarshal(resp.Data, &kbSearchRes)
+		if err != nil {
+			err = errors.New("搜索知识库失败")
+			return
+		}
+		content = kbSearchRes
+		return
+	}
+	err = errors.New("搜索知识库失败")
+	return
+}
+func init() {
+	err := llm.Register(llm.ETA_LLM_CLIENT, GetInstance())
+	if err != nil {
+		utils.FileLog.Error("注册eta_llm_server服务失败:", err)
+	}
+}
+
+func (ds *ETALLMClient) DoPost(apiUrl string, body []byte) (baseResp eta_llm_http.BaseResponse, err error) {
+	requestReader := bytes.NewReader(body)
+	response, err := ds.HttpClient.Post(ds.BaseURL+apiUrl, CONTENT_TYPE_JSON, requestReader)
+	if err != nil {
+		return
+	}
+	return parseResponse(response)
+}
+func (ds *ETALLMClient) DoStreamPost(apiUrl string, body []byte) (baseResp *http.Response, err error) {
+	requestReader := bytes.NewReader(body)
+	return ds.HttpClient.Post(ds.BaseURL+apiUrl, CONTENT_TYPE_JSON, requestReader)
+}
+func parseResponse(response *http.Response) (baseResp eta_llm_http.BaseResponse, err error) {
+	defer func() {
+		_ = response.Body.Close()
+	}()
+	baseResp.Ret = response.StatusCode
+	if response.StatusCode != http.StatusOK {
+		baseResp.Msg = fmt.Sprintf("请求失败,状态码:%d, 状态信息:%s", response.StatusCode, http.StatusText(response.StatusCode))
+		return
+	}
+	bodyBytes, err := io.ReadAll(response.Body)
+	if err != nil {
+		err = fmt.Errorf("读取响应体失败: %w", err)
+		return
+	}
+	baseResp.Success = true
+	baseResp.Data = bodyBytes
+	return
+}
+func ParseStreamResponse(response *http.Response) (contentChan chan string, errChan chan error, closeChan chan struct{}) {
+	contentChan = make(chan string, 10)
+	errChan = make(chan error, 10)
+	closeChan = make(chan struct{})
+	go func() {
+		defer close(contentChan)
+		defer close(errChan)
+		defer close(closeChan)
+		scanner := bufio.NewScanner(response.Body)
+		scanner.Split(bufio.ScanLines)
+		for scanner.Scan() {
+			line := scanner.Text()
+			if line == "" {
+				continue
+			}
+			// 忽略 "ping" 行
+			if strings.HasPrefix(line, ": ping") {
+				continue
+			}
+			// 去除 "data: " 前缀
+			if strings.HasPrefix(line, "data: ") {
+				line = strings.TrimPrefix(line, "data: ")
+			}
+			var chunk eta_llm_http.ChunkResponse
+			if err := json.Unmarshal([]byte(line), &chunk); err != nil {
+				fmt.Println("解析错误的line:" + line)
+				errChan <- fmt.Errorf("解析 JSON 块失败: %w", err)
+				return
+			}
+			// 处理每个 chunk
+			if chunk.Choices != nil && len(chunk.Choices) > 0 {
+				for _, choice := range chunk.Choices {
+					if choice.Delta.Content != "" {
+						contentChan <- choice.Delta.Content
+					}
+				}
+			}
+		}
+		if err := scanner.Err(); err != nil {
+			errChan <- fmt.Errorf("读取响应体失败: %w", err)
+			return
+		}
+	}()
+	return
+}

+ 44 - 0
utils/llm/eta_llm/eta_llm_http/request.go

@@ -0,0 +1,44 @@
+package eta_llm_http
+
+type KbChatRequest struct {
+	Query          string           `json:"query"`
+	Mode           string           `json:"mode"`
+	KbName         string           `json:"kb_name"`
+	TopK           int              `json:"top_k"`
+	ScoreThreshold float32          `json:"score_threshold"`
+	History        []HistoryContent `json:"history"`
+	Stream         bool             `json:"stream"`
+	Model          string           `json:"model"`
+	Temperature    float32          `json:"temperature"`
+	MaxTokens      int              `json:"max_tokens"`
+	PromptName     string           `json:"prompt_name"`
+	ReturnDirect   bool             `json:"return_direct"`
+}
+type DocumentChatRequest struct {
+	Query          string           `json:"query"`
+	KnowledgeId    string           `json:"knowledge_id"`
+	TopK           int              `json:"top_k"`
+	ScoreThreshold float32          `json:"score_threshold"`
+	History        []HistoryContent `json:"history"`
+	Stream         bool             `json:"stream"`
+	ModelName      string           `json:"model_name"`
+	Temperature    float32          `json:"temperature"`
+	MaxTokens      int              `json:"max_tokens"`
+	PromptName     string           `json:"prompt_name"`
+}
+type HistoryContent struct {
+	Content string `json:"content"`
+	Role    string `json:"role"`
+}
+type HistoryContentWeb struct {
+	Content string `json:"Content"`
+	Role    string `json:"Role"`
+}
+type KbSearchDocsRequest struct {
+	Query             string      `json:"query"`
+	KnowledgeBaseName string      `json:"knowledge_base_name"`
+	TopK              int         `json:"top_k"`
+	ScoreThreshold    float32     `json:"score_threshold"`
+	FileName          string      `json:"file_name"`
+	Metadata          interface{} `json:"metadata"`
+}

+ 63 - 0
utils/llm/eta_llm/eta_llm_http/response.go

@@ -0,0 +1,63 @@
+package eta_llm_http
+
+import "encoding/json"
+
+type BaseResponse struct {
+	Ret     int             `json:"ret"`
+	Msg     string          `json:"msg"`
+	Success bool            `json:"success"`
+	Data    json.RawMessage `json:"data"`
+}
+type SteamResponse struct {
+	Data    ChunkResponse `json:"data"`
+}
+// ChunkResponse 定义流式响应的结构体
+type ChunkResponse struct {
+	ID          string   `json:"id"`
+	Object      string   `json:"object"`
+	Model       string   `json:"model"`
+	Created     int64    `json:"created"`
+	Status      *string  `json:"status"`
+	MessageType int      `json:"message_type"`
+	MessageID   *string  `json:"message_id"`
+	IsRef       bool     `json:"is_ref"`
+	Docs        []string `json:"docs"`
+	Choices     []Choice `json:"choices"`
+}
+
+// Choice 定义选择的结构体
+type Choice struct {
+	Delta Delta  `json:"delta"`
+	Role  string `json:"role"`
+}
+
+// Delta 定义增量的结构体
+type Delta struct {
+	Content   string     `json:"content"`
+	ToolCalls []ToolCall `json:"tool_calls"`
+}
+
+// ToolCall 定义工具调用的结构体
+type ToolCall struct {
+	ID       string   `json:"id"`
+	Type     string   `json:"type"`
+	Function Function `json:"function"`
+}
+
+// Function 定义函数的结构体
+type Function struct {
+	Name      string          `json:"name"`
+	Arguments json.RawMessage `json:"arguments"`
+}
+
+type SearchDocsResponse struct {
+	PageContent string   `json:"page_content"`
+	Metadata    Metadata `json:"metadata"`
+	Type        string   `json:"type"`
+	Id          string   `json:"id"`
+	Score       float32  `json:"score"`
+}
+type Metadata struct {
+	Source string `json:"source"`
+	Id     string `json:"id"`
+}

+ 27 - 0
utils/llm/llm_client.go

@@ -0,0 +1,27 @@
+package llm
+
+import (
+	"encoding/json"
+	"net/http"
+	"time"
+)
+
+type LLMClient struct {
+	BaseURL    string
+	HttpClient *http.Client
+}
+
+func NewLLMClient(baseURL string, timeout time.Duration) *LLMClient {
+	return &LLMClient{
+		BaseURL: baseURL,
+		HttpClient: &http.Client{
+			Timeout: timeout * time.Second,
+		},
+	}
+}
+
+type LLMService interface {
+	KnowledgeBaseChat(query string, KnowledgeBaseName string, history []json.RawMessage) (llmRes *http.Response, err error)
+	DocumentChat(query string, KnowledgeId string, history []json.RawMessage, stream bool) (llmRes *http.Response, err error)
+	SearchKbDocs(query string, KnowledgeBaseName string) (data interface{}, err error)
+}

+ 38 - 0
utils/llm/llm_factory.go

@@ -0,0 +1,38 @@
+package llm
+
+import (
+	"errors"
+)
+
+var (
+	llmInstanceMap = make(map[string]LLMService)
+)
+
+const (
+	ETA_LLM_CLIENT = "eta_llm"
+)
+
+func Register(name string, llmClient LLMService) (err error) {
+	if name == "" {
+		err = errors.New("模型实例名不能为空")
+		return
+	}
+	if _, ok := llmInstanceMap[name]; ok {
+		err = errors.New("模型实例已经存在")
+		return
+	}
+	llmInstanceMap[name] = llmClient
+	return
+}
+func GetInstance(name string) (llmClient LLMService, err error) {
+	if name == "" {
+		err = errors.New("模型实例名不能为空")
+		return
+	}
+	if _, ok := llmInstanceMap[name]; !ok {
+		err = errors.New("当前模型类型不支持")
+		return
+	}
+	llmClient = llmInstanceMap[name]
+	return
+}

+ 106 - 0
utils/lock/distrubtLock.go

@@ -0,0 +1,106 @@
+package lock
+
+import (
+	"context"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/go-redis/redis/v8"
+	"time"
+)
+
+const (
+	lockName = "lock:"
+)
+
+var (
+	ctx = context.Background()
+)
+
+func AcquireLock(key string, expiration int, Holder string) bool {
+	script := redis.NewScript(`local key = KEYS[1]
+			local clientId = ARGV[1]
+			local expiration = tonumber(ARGV[2])
+			if redis.call("EXISTS", key) == 0 then
+				redis.call("SET", key, clientId, "EX", expiration)
+				return 1
+			else
+				return 0
+			end`)
+	lockey := fmt.Sprintf("%s%s", lockName, key)
+	result, err := script.Run(ctx, utils.Rc.RedisClient(), []string{lockey}, Holder, expiration).Int()
+	if err != nil {
+		fmt.Printf("加锁失败:err: %v", err)
+		return false
+	}
+	if result == 1 {
+		return true
+	}
+	return false
+}
+
+func TryLock(key string, expiration int, Holder string, timeout time.Duration) error {
+	redisCtx, cancel := context.WithTimeout(context.Background(), timeout)
+	defer cancel()
+	for {
+		select {
+		case <-redisCtx.Done():
+			return fmt.Errorf("等待超时")
+		default:
+			if AcquireLock(key, expiration, Holder) {
+				// 启动异步续约
+				go renewLock(key, Holder, expiration)
+				return nil
+			}
+			// 等待重试
+			time.Sleep(50 * time.Millisecond)
+		}
+	}
+}
+
+func renewLock(key, holder string, expiration int) {
+	for {
+		time.Sleep(5 * time.Second)
+		script := redis.NewScript(`
+            local key = KEYS[1]
+			local clientId = ARGV[1]
+			local expiration = tonumber(ARGV[2])
+            if redis.call("get", KEYS[1]) == ARGV[1] then
+                redis.call("SET", key, clientId, "EX", expiration)
+				 return 1
+            else
+				 return 0
+            end
+       `)
+		lockey := fmt.Sprintf("%s%s", lockName, key)
+		result, err := script.Run(context.Background(), utils.Rc.RedisClient(), []string{lockey}, holder, expiration).Result()
+		if err != nil {
+			fmt.Println("锁续期失败:", err)
+			return
+		}
+		if result.(int64) == 0 {
+			fmt.Println("持有者变更,停止监听")
+			return
+		}
+		fmt.Printf("锁续期成功")
+	}
+}
+
+func ReleaseLock(key string, holder string) bool {
+	script := redis.NewScript(`
+	   if redis.call("get", KEYS[1]) == ARGV[1] then
+	       return redis.call("del", KEYS[1])
+	   else
+	       return 0
+	   end
+	`)
+	lockey := fmt.Sprintf("%s%s", lockName, key)
+	result, err := script.Run(ctx, utils.Rc.RedisClient(), []string{lockey}, holder).Int()
+	if err != nil {
+		fmt.Printf("解锁失败:err: %v", err)
+		return false
+	}
+	if result == 1 {
+		return true
+	}
+	return false
+}

+ 6 - 1
utils/redis.go

@@ -2,6 +2,7 @@ package utils
 
 import (
 	"eta/eta_api/utils/redis"
+	client "github.com/go-redis/redis/v8"
 	"time"
 )
 
@@ -24,6 +25,11 @@ type RedisClient interface {
 	SAdd(key string, args ...interface{}) (err error)
 	SRem(key string, args ...interface{}) (err error)
 	SIsMember(key string, args interface{}) (bool, error)
+	ZAdd(key string, members ...*redis.Zset) error
+	ZRangeWithScores(key string) ([]*redis.Zset, error)
+	Expire(key string, duration time.Duration) error
+	RedisClient() client.UniversalClient
+	Keys(pattern string) ([]string, error)
 }
 
 func initRedis(redisType string, conf string) (redisClient RedisClient, err error) {
@@ -33,7 +39,6 @@ func initRedis(redisType string, conf string) (redisClient RedisClient, err erro
 	default: // 默认走单机
 		redisClient, err = redis.InitStandaloneRedis(conf)
 	}
-
 	return
 }
 

+ 37 - 0
utils/redis/cluster_redis.go

@@ -16,6 +16,10 @@ import (
 type ClusterRedisClient struct {
 	redisClient *redis.ClusterClient
 }
+type Zset struct {
+	Score  float64
+	Member interface{}
+}
 
 var DefaultKey = "zcmRedis"
 
@@ -317,3 +321,36 @@ func (rc *ClusterRedisClient) SIsMember(key string, args interface{}) (isMember
 	isMember, err = rc.redisClient.SIsMember(context.TODO(), key, args).Result()
 	return
 }
+func (rc *ClusterRedisClient) ZAdd(key string, members ...*Zset) error {
+	var redisMembers []*redis.Z
+	for _, member := range members {
+		redisMembers = append(redisMembers, &redis.Z{
+			Member: member.Member,
+			Score:  member.Score,
+		})
+	}
+	return rc.redisClient.ZAdd(context.TODO(), key, redisMembers...).Err()
+}
+func (rc *ClusterRedisClient) ZRangeWithScores(key string) (result []*Zset, err error) {
+	redisZList, err := rc.redisClient.ZRangeWithScores(context.TODO(), key, 0, -1).Result()
+	if err != nil {
+		return
+	}
+	for _, redisZ := range redisZList {
+		result = append(result, &Zset{
+			Member: redisZ.Member,
+			Score:  redisZ.Score,
+		})
+	}
+	return
+}
+func (rc *ClusterRedisClient) Expire(key string, duration time.Duration) error {
+	return rc.redisClient.Expire(context.Background(), key, duration).Err()
+}
+
+func (rc *ClusterRedisClient) Keys(pattern string) (keys []string, err error) {
+	return rc.redisClient.Keys(context.Background(), pattern).Result()
+}
+func (rc *ClusterRedisClient) RedisClient() redis.UniversalClient {
+	return rc.redisClient
+}

+ 34 - 0
utils/redis/standalone_redis.go

@@ -309,3 +309,37 @@ func (rc *StandaloneRedisClient) SIsMember(key string, args interface{}) (isMemb
 	isMember, err = rc.redisClient.SIsMember(context.TODO(), key, args).Result()
 	return
 }
+
+func (rc *StandaloneRedisClient) ZAdd(key string, members ...*Zset) error {
+	var redisMembers []*redis.Z
+	for _, member := range members {
+		redisMembers = append(redisMembers, &redis.Z{
+			Member: member.Member,
+			Score:  member.Score,
+		})
+	}
+	return rc.redisClient.ZAdd(context.TODO(), key, redisMembers...).Err()
+}
+
+func (rc *StandaloneRedisClient) ZRangeWithScores(key string) (result []*Zset, err error) {
+	redisZList, err := rc.redisClient.ZRangeWithScores(context.TODO(), key, 0, -1).Result()
+	if err != nil {
+		return
+	}
+	for _, redisZ := range redisZList {
+		result = append(result, &Zset{
+			Member: redisZ.Member,
+			Score:  redisZ.Score,
+		})
+	}
+	return
+}
+func (rc *StandaloneRedisClient) Expire(key string, duration time.Duration) error {
+	return rc.redisClient.Expire(context.Background(), key, duration).Err()
+}
+func (rc *StandaloneRedisClient) Keys(pattern string) (keys []string, err error) {
+	return rc.redisClient.Keys(context.Background(), pattern).Result()
+}
+func (rc *StandaloneRedisClient) RedisClient() redis.UniversalClient {
+	return rc.redisClient
+}

+ 72 - 0
utils/sql.go

@@ -20,6 +20,8 @@ const (
 	Order         SqlCondition = "Order"
 	Delimiter     SqlCondition = "Delimiter"
 	ConvertColumn SqlCondition = "ConvertColumn"
+
+	ToDate SqlCondition = "ToDate"
 )
 
 var TemplateMap = map[SqlCondition]map[Driver]string{
@@ -31,6 +33,10 @@ var TemplateMap = map[SqlCondition]map[Driver]string{
 		MySql: `CONVERT({{.ConvertColumn}} USING gbk )`,
 		DM:    `{{.ConvertColumn}}`,
 	},
+	ToDate: {
+		MySql: `DATE({{.Column}})`,
+		DM:    `TO_DATE({{.Column}})`,
+	},
 }
 
 var supportDriverMap = map[string]Driver{
@@ -67,6 +73,71 @@ func (distinctParam *DistinctParam) GetFormatConditionStr(param *QueryParam) str
 	return ""
 }
 
+type ToDateParam struct {
+}
+
+func (toDateParam *ToDateParam) GetParamName() string {
+	return "ToDate"
+}
+func (toDateParam *ToDateParam) GetFormatConditionStr(param *QueryParam) (sql string) {
+	dbDriver, _ := getDriverInstance(param.Driver)
+	if param.Column == "" {
+		FileLog.Error("聚合字段为空,无法生成聚合sql")
+		return
+	}
+	var templateSqlStr string
+	if _, ok := TemplateMap[ToDate][dbDriver]; !ok {
+		templateSqlStr = TemplateMap[ToDate][MySql]
+	} else {
+		templateSqlStr = TemplateMap[ToDate][dbDriver]
+	}
+	if templateSqlStr == "" {
+		FileLog.Error("聚合sql模板不存在,无法生成聚合sql")
+		return
+	}
+	templateSql, err := template.New("ToDate").Parse(templateSqlStr)
+	if err != nil {
+		FileLog.Error("failed to parse template: %v", err)
+		return
+	}
+	//反射获取结构体的值
+	value := reflect.ValueOf(param)
+	// 检查是否是指针
+	if value.Kind() != reflect.Ptr {
+		fmt.Println("请求参数必须是一个结构体")
+		return
+	}
+	// 获取结构体的元素
+	elem := value.Elem()
+	// 检查是否是结构体
+	if elem.Kind() != reflect.Struct {
+		fmt.Println("请求参数必须是一个结构体")
+		return
+	}
+	// 获取字段的值
+	fieldValue := elem.FieldByName("ConvertColumn")
+	// 检查字段是否存在
+	if !fieldValue.IsValid() {
+		fmt.Printf("Error: field %s not found\n", "ConvertColumn")
+		return
+	}
+	// 检查字段是否可导出
+	if !fieldValue.CanSet() {
+		fmt.Printf("Error: field %s is not exported and cannot be set\n", "ConvertColumn")
+		return
+	}
+	// 渲染模板
+	var buf bytes.Buffer
+	err = templateSql.Execute(&buf, param)
+	if err != nil {
+		fmt.Sprintf("执行模板填充失败: %v", err)
+		return
+	}
+	sql = buf.String()
+	fmt.Printf("生成的转换日期语句为:%s\n", sql)
+	return sql
+}
+
 type ConvertParam struct {
 }
 
@@ -145,6 +216,7 @@ var sqlGeneratorFactory = map[SqlCondition]SqlParam{
 	Delimiter:     &DelimiterParam{},
 	Distinct:      &DistinctParam{},
 	ConvertColumn: &ConvertParam{},
+	ToDate:        &ToDateParam{},
 }
 
 type DelimiterParam struct {

+ 93 - 0
utils/ws/latency_measurer.go

@@ -0,0 +1,93 @@
+package ws
+
+import (
+	"errors"
+	"github.com/gorilla/websocket"
+	"sync"
+	"time"
+)
+
+const (
+	maxMessageSize   = 1024 * 1024 * 10 // 1MB
+	basePingInterval = 5 * time.Second
+	maxPingInterval  = 120 * time.Second
+	minPingInterval  = 15 * time.Second
+)
+
+// LatencyMeasurer 延迟测量器
+type LatencyMeasurer struct {
+	measurements    []time.Duration
+	lastLatency     time.Duration
+	mu              sync.Mutex
+	lastPingTime    time.Time // 最后一次发送Ping的时间
+	maxMeasurements int       // 保留的最大测量次数
+}
+
+func NewLatencyMeasurer(windowSize int) *LatencyMeasurer {
+	return &LatencyMeasurer{
+		maxMeasurements: windowSize,
+		measurements:    make([]time.Duration, 0, windowSize),
+		lastLatency:     basePingInterval,
+	}
+}
+
+// 发送Ping并记录时间戳
+func (lm *LatencyMeasurer) SendPing(conn *websocket.Conn) error {
+	lm.mu.Lock()
+	defer lm.mu.Unlock()
+	if conn == nil {
+		return errors.New("connection closed")
+	}
+	// 发送Ping消息
+	err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWaitTimeout))
+	if err != nil {
+		return err
+	}
+	lm.lastPingTime = time.Now()
+	return nil
+}
+
+// 处理Pong响应
+func (lm *LatencyMeasurer) CalculateLatency() {
+	lm.mu.Lock()
+	defer lm.mu.Unlock()
+	if lm.lastPingTime.IsZero() {
+		return
+	}
+	// 计算往返时间
+	rtt := time.Since(lm.lastPingTime)
+	// 维护滑动窗口
+	if len(lm.measurements) >= lm.maxMeasurements {
+		lm.measurements = lm.measurements[1:]
+	}
+	lm.measurements = append(lm.measurements, rtt)
+	// 计算平均延迟(可根据需求改为中位数等)
+	sum := time.Duration(0)
+	for _, d := range lm.measurements {
+		sum += d
+	}
+	lm.lastLatency = sum / time.Duration(len(lm.measurements))
+	if lm.lastLatency > maxPingInterval {
+		lm.lastLatency = maxPingInterval
+	}
+	if lm.lastLatency < minPingInterval {
+		lm.lastLatency = minPingInterval
+	}
+}
+
+// 获取当前网络延迟估值
+func (lm *LatencyMeasurer) GetLatency() time.Duration {
+	lm.mu.Lock()
+	defer lm.mu.Unlock()
+	return lm.lastLatency
+}
+
+// 在连接初始化时设置Pong处理器
+func SetupLatencyMeasurement(conn *websocket.Conn) *LatencyMeasurer {
+	lm := NewLatencyMeasurer(5) // 使用最近5次测量的滑动窗口
+	conn.SetPongHandler(func(appData string) error {
+		lm.CalculateLatency()
+		return nil
+	})
+	return lm
+}

+ 113 - 0
utils/ws/limiter.go

@@ -0,0 +1,113 @@
+package ws
+
+import (
+	"fmt"
+	"golang.org/x/time/rate"
+	"sync"
+	"time"
+)
+
+var (
+	limiterManagers map[string]*LimiterManger
+	limiterOnce     sync.Once
+	limiters        = map[string]LimiterConfig{
+		QA_LIMITER: {
+			LimiterKey: LIMITER_KEY,
+			Duration:   RATE_LIMTER_TIME,
+		},
+		CONNECT_LIMITER: {
+			LimiterKey: CONNECT_LIMITER_KEY,
+			Duration:   CONNECT_LIMITER_TIME,
+		},
+	}
+)
+
+type LimiterConfig struct {
+	LimiterKey string
+	Duration   time.Duration
+}
+
+const (
+	CONNECT_LIMITER     = "connetLimiter"
+	QA_LIMITER          = "qaLimiter"
+	LIMITER_KEY         = "llm_chat_key_user_%d"
+	CONNECT_LIMITER_KEY = "llm_chat_connect_key_user_%d"
+
+	RATE_LIMTER_TIME     = 10 * time.Second
+	CONNECT_LIMITER_TIME = 200 * time.Millisecond
+)
+
+var ()
+
+type RateLimiter struct {
+	LastRequest time.Time
+	Duration    time.Duration
+	*rate.Limiter
+}
+type LimiterManger struct {
+	sync.RWMutex
+	limiterMap map[string]*RateLimiter
+}
+
+//func (qaLimiter *QALimiter) Allow() bool {
+//	return qaLimiter.Limiter.Allow()
+//}
+
+// GetLimiter 获取或创建用户的限流器
+func (qalm *LimiterManger) GetLimiter(token string, limiterKey string) (limiter *RateLimiter, duration time.Duration) {
+	qalm.Lock()
+	defer qalm.Unlock()
+	if config, ok := limiters[limiterKey]; !ok {
+		duration = 0 * time.Second
+	} else {
+		duration = config.Duration
+	}
+	if target, exists := qalm.limiterMap[token]; exists {
+		limiter = target
+		return
+	}
+	// 创建一个新的限流器,例如每10秒1个请求
+	limiter = &RateLimiter{
+		Limiter: rate.NewLimiter(rate.Every(duration), 1),
+	}
+	qalm.limiterMap[token] = limiter
+	return
+}
+func (qalm *LimiterManger) Allow(token string, limiterKey string) bool {
+	limiter, duration := qalm.GetLimiter(token, limiterKey)
+	if limiter.LastRequest.IsZero() {
+		limiter.LastRequest = time.Now()
+		return limiter.Allow()
+	}
+	if time.Now().Sub(limiter.LastRequest) < duration {
+		return false
+	}
+	limiter.LastRequest = time.Now()
+	return limiter.Allow()
+}
+func getInstance(key string) *LimiterManger {
+	limiterOnce.Do(func() {
+		if limiterManagers == nil {
+			limiterManagers = make(map[string]*LimiterManger, len(limiters))
+		}
+		for key = range limiters {
+			limiterManagers[key] = &LimiterManger{
+				limiterMap: make(map[string]*RateLimiter),
+			}
+		}
+	})
+	return limiterManagers[key]
+}
+
+func Allow(userId int, limiter string) bool {
+	config := limiters[limiter]
+	if config.LimiterKey == "" {
+		return false
+	}
+	token := fmt.Sprintf(config.LimiterKey, userId)
+	handler := getInstance(limiter)
+	if handler == nil {
+		return false
+	}
+	return handler.Allow(token, limiter)
+}

+ 163 - 0
utils/ws/session.go

@@ -0,0 +1,163 @@
+package ws
+
+import (
+	"encoding/json"
+	"errors"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/gorilla/websocket"
+	"sync"
+	"time"
+)
+
+// Session 会话结构
+type Session struct {
+	Id          string
+	UserId      int
+	Conn        *websocket.Conn
+	LastActive  time.Time
+	Latency     *LatencyMeasurer
+	History     []json.RawMessage
+	CloseChan   chan struct{}
+	MessageChan chan string
+	mu          sync.RWMutex
+	sessionOnce sync.Once
+}
+
+type Message struct {
+	KbName string `json:"KbName"`
+	Query  string `json:"Query"`
+	ChatId int    `json:"ChatId"`
+	//LastTopics []json.RawMessage `json:"LastTopics"`
+}
+
+// readPump 处理读操作
+func (s *Session) readPump() {
+	defer func() {
+		fmt.Printf("读进程session %s closed", s.Id)
+		manager.RemoveSession(s.Id)
+	}()
+	s.Conn.SetReadLimit(maxMessageSize)
+	_ = s.Conn.SetReadDeadline(time.Now().Add(ReadTimeout))
+	for {
+		_, message, err := s.Conn.ReadMessage()
+		if err != nil {
+			fmt.Printf("websocket 错误关闭 %s closed", err.Error())
+			handleCloseError(err)
+			return
+		}
+		// 更新活跃时间
+		s.UpdateActivity()
+		// 处理消息
+		if err = manager.HandleMessage(s.UserId, s.Id, message); err != nil {
+			//写应答
+			_ = s.writeWithTimeout("<think></think>")
+			_ = s.writeWithTimeout(err.Error())
+			_ = s.writeWithTimeout("<EOF/>")
+		}
+	}
+}
+
+// UpdateActivity 跟新最近活跃时间
+func (s *Session) UpdateActivity() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	s.LastActive = time.Now()
+}
+
+func (s *Session) Close() {
+	s.sessionOnce.Do(func() {
+		// 控制关闭顺序
+		close(s.CloseChan)
+		close(s.MessageChan)
+		s.forceClose()
+	})
+}
+
+// 带超时的安全写入
+func (s *Session) writeWithTimeout(msg string) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	if s.Conn == nil {
+		utils.FileLog.Error("写入消息失败,connection已关闭")
+		return errors.New("connection closed")
+	}
+	// 设置写超时
+	if err := s.Conn.SetWriteDeadline(time.Now().Add(writeWaitTimeout)); err != nil {
+		return err
+	}
+	return s.Conn.WriteMessage(websocket.TextMessage, []byte(msg))
+}
+
+// writePump 处理写操作
+func (s *Session) writePump() {
+	ticker := time.NewTicker(basePingInterval)
+	defer func() {
+		fmt.Printf("写继进程:session %s closed", s.Id)
+		manager.RemoveSession(s.Id)
+		ticker.Stop()
+	}()
+	for {
+		select {
+		case message, ok := <-s.MessageChan:
+			if !ok {
+				return
+			}
+			_ = s.writeWithTimeout(message)
+		case <-ticker.C:
+			_ = s.Latency.SendPing(s.Conn)
+			ticker.Reset(s.Latency.lastLatency)
+		case <-s.CloseChan:
+			return
+		}
+	}
+}
+func handleCloseError(err error) {
+	utils.FileLog.Error("websocket错误关闭 %s closed", err.Error())
+	if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
+		var wsErr *websocket.CloseError
+		if !errors.As(err, &wsErr) {
+			fmt.Printf("websocket未知错误 %s", err.Error())
+			utils.FileLog.Error("未知错误 %s", err.Error())
+		} else {
+			switch wsErr.Code {
+			case websocket.CloseNormalClosure:
+				fmt.Println("websocket正常关闭连接")
+				utils.FileLog.Info("正常关闭连接")
+			default:
+				fmt.Printf("websocket关闭代码 %d:%s", wsErr.Code, wsErr.Text)
+				utils.FileLog.Error(":%d:%s", wsErr.Code, wsErr.Text)
+			}
+		}
+	}
+}
+
+// 强制关闭连接
+func (s *Session) forceClose() {
+	// 添加互斥锁保护
+	s.mu.Lock()
+	defer s.mu.Unlock()
+	// 发送关闭帧
+	_ = s.Conn.WriteControl(websocket.CloseMessage,
+		websocket.FormatCloseMessage(websocket.ClosePolicyViolation, "heartbeat failed"),
+		time.Now().Add(writeWaitTimeout))
+	_ = s.Conn.Close()
+	s.Conn = nil // 标记连接已关闭
+	utils.FileLog.Info("连接已强制关闭",
+		"user", s.UserId,
+		"session", s.Id)
+}
+func NewSession(userId int, sessionId string, conn *websocket.Conn) (session *Session) {
+	session = &Session{
+		UserId:      userId,
+		Id:          sessionId,
+		Conn:        conn,
+		LastActive:  time.Now(),
+		CloseChan:   make(chan struct{}),
+		MessageChan: make(chan string, 10),
+	}
+	session.Latency = SetupLatencyMeasurement(conn)
+	go session.readPump()
+	go session.writePump()
+	return
+}

+ 224 - 0
utils/ws/session_manager.go

@@ -0,0 +1,224 @@
+package ws
+
+import (
+	"encoding/json"
+	"errors"
+	chatService "eta/eta_api/services/llm"
+	"eta/eta_api/utils"
+	"eta/eta_api/utils/llm"
+	"eta/eta_api/utils/llm/eta_llm"
+	"eta/eta_api/utils/llm/eta_llm/eta_llm_http"
+	"fmt"
+	"github.com/gorilla/websocket"
+	"net/http"
+	"strings"
+	"sync"
+	"time"
+)
+
+var (
+	llmService, _ = llm.GetInstance(llm.ETA_LLM_CLIENT)
+)
+
+const (
+	defaultCheckInterval = 2 * time.Minute  // 检测间隔应小于心跳超时时间
+	connectionTimeout    = 10 * time.Minute // 客户端超时时间
+	TcpTimeout           = 20 * time.Minute // TCP超时时间,保底关闭,覆盖会话超时时间
+	ReadTimeout          = 15 * time.Minute // 读取超时时间,保底关闭,覆盖会话超时时间
+	writeWaitTimeout     = 60 * time.Second //写入超时时间
+)
+
+type ConnectionManager struct {
+	Sessions sync.Map
+	ticker   *time.Ticker
+	stopChan chan struct{}
+}
+
+var (
+	smOnce  sync.Once
+	manager *ConnectionManager
+)
+
+func GetInstance() *ConnectionManager {
+	smOnce.Do(func() {
+		if manager == nil {
+			manager = &ConnectionManager{
+				ticker:   time.NewTicker(defaultCheckInterval),
+				stopChan: make(chan struct{}),
+			}
+		}
+	})
+	return manager
+}
+func Manager() *ConnectionManager {
+	return manager
+}
+
+// HandleMessage 消息处理核心逻辑
+func (manager *ConnectionManager) HandleMessage(userID int, sessionID string, message []byte) error {
+
+	session, exists := manager.GetSession(sessionID)
+	if !exists {
+		return errors.New("session not found")
+	}
+	if strings.ToLower(string(message)) == "pong" {
+		session.UpdateActivity()
+		fmt.Printf("收到心跳消息,续期长连接:%v", session.LastActive)
+		return nil
+	}
+	if !Allow(userID, QA_LIMITER) {
+		_ = session.Conn.WriteMessage(websocket.TextMessage, []byte("<think></think>"))
+		_ = session.Conn.WriteMessage(websocket.TextMessage, []byte("您提问的太频繁了,请稍后再试"))
+		_ = session.Conn.WriteMessage(websocket.TextMessage, []byte("<EOF/>"))
+		return nil
+	}
+	var userMessage Message
+	err := json.Unmarshal(message, &userMessage)
+	if err != nil {
+		utils.FileLog.Error(fmt.Sprintf("消息格式错误:%s", string(message)))
+		fmt.Printf("消息格式错误:%s", string(message))
+		return errors.New("消息格式错误:" + err.Error())
+	}
+	// 处理业务逻辑
+	//session.History = append(session.History, userMessage.LastTopics...)
+	redisHisChat, err := chatService.GetChatRecordsFromRedis(userMessage.ChatId)
+	if err != nil {
+		utils.FileLog.Error("获取历史对话数据失败,err:", err.Error())
+	} else {
+		for _, chat := range redisHisChat {
+			his := eta_llm_http.HistoryContent{
+				Content: chat.Content,
+				Role:    chat.ChatUserType,
+			}
+			hisMsg, _ := json.Marshal(&his)
+			if len(hisMsg) != 0 {
+				session.History = append(session.History, hisMsg)
+			}
+		}
+	}
+	resp, err := llmService.KnowledgeBaseChat(userMessage.Query, userMessage.KbName, session.History)
+	defer func() {
+		if resp != nil && resp.Body != nil && err == nil {
+			_ = resp.Body.Close()
+		}
+	}()
+	if resp == nil {
+		utils.FileLog.Error("知识库问答失败: 无应答")
+		return errors.New("知识库问答失败: 无应答")
+	}
+	if err != nil {
+		utils.FileLog.Error(fmt.Sprintf("知识库问答失败: httpCode:%d,错误信息:%s", resp.StatusCode, http.StatusText(resp.StatusCode)))
+		err = errors.New(fmt.Sprintf("知识库问答失败: httpCode:%d,错误信息:%s", resp.StatusCode, http.StatusText(resp.StatusCode)))
+		return err
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		utils.FileLog.Error(fmt.Sprintf("知识库问答失败: httpCode:%d,错误信息:%s", resp.StatusCode, http.StatusText(resp.StatusCode)))
+		err = errors.New(fmt.Sprintf("知识库问答失败: httpCode:%d,错误信息:%s", resp.StatusCode, http.StatusText(resp.StatusCode)))
+		return err
+	}
+	// 解析流式响应
+	contentChan, errChan, closeChan := eta_llm.ParseStreamResponse(resp)
+	emptyContent := true
+	// 处理流式数据并发送到 WebSocket
+	for {
+		select {
+		case content, ok := <-contentChan:
+			if !ok {
+				err = errors.New("未知的错误异常")
+				return err
+			}
+			session.UpdateActivity()
+			if emptyContent {
+				emptyContent = false
+			}
+			// 发送消息到 WebSocket
+			_ = session.Conn.WriteMessage(websocket.TextMessage, []byte(content))
+		case chanErr, ok := <-errChan:
+			if !ok {
+				err = errors.New("未知的错误异常")
+			} else {
+				err = errors.New(chanErr.Error())
+			}
+			// 发送错误消息到 WebSocket
+			return err
+		case <-closeChan:
+			if emptyContent {
+				_ = session.Conn.WriteMessage(websocket.TextMessage, []byte("<think></think>"))
+			}
+			_ = session.Conn.WriteMessage(websocket.TextMessage, []byte("<EOF/>"))
+			return nil
+		}
+	}
+	// 更新最后活跃时间
+	// 发送响应
+	//return session.Conn.WriteMessage(websocket.TextMessage, []byte(response))
+}
+
+// AddSession Add 添加一个新的会话
+func (manager *ConnectionManager) AddSession(session *Session) {
+	manager.Sessions.Store(session.Id, session)
+}
+func (manager *ConnectionManager) GetSessionId(userId int, sessionId string) (sessionID string) {
+	return fmt.Sprintf("%d_%s", userId, sessionId)
+}
+
+// RemoveSession Remove 移除一个会话
+func (manager *ConnectionManager) RemoveSession(sessionCode string) {
+	fmt.Printf("移除会话: SessionID=%s, UserID=%s", sessionCode, sessionCode)
+	manager.Sessions.Delete(sessionCode)
+}
+
+// GetSession 获取一个会话
+func (manager *ConnectionManager) GetSession(sessionCode string) (session *Session, exists bool) {
+	if data, ok := manager.Sessions.Load(sessionCode); ok {
+		session = data.(*Session)
+		exists = ok
+	}
+	return
+}
+
+// CheckAll 批量检测所有连接
+func (manager *ConnectionManager) CheckAll() {
+	manager.Sessions.Range(func(key, value interface{}) bool {
+		session := value.(*Session)
+		// 判断超时
+		if time.Since(session.LastActive) > 2*connectionTimeout {
+			fmt.Printf("连接超时关闭: SessionID=%s, UserID=%s", session.Id, session.UserId)
+			utils.FileLog.Warn("连接超时关闭: SessionID=%s, UserID=%s", session.Id, session.UserId)
+			session.Close()
+			return true
+		}
+		// 发送心跳
+		go func(s *Session) {
+			err := s.Conn.WriteControl(websocket.PingMessage,
+				nil, time.Now().Add(writeWaitTimeout))
+			if err != nil {
+				fmt.Printf("心跳发送失败: SessionID=%s, Error=%v", s.Id, err)
+				utils.FileLog.Warn("心跳发送失败: SessionID=%s, Error=%v",
+					s.Id, err)
+				fmt.Println("心跳无响应,退出请求")
+				session.Close()
+			}
+		}(session)
+		return true
+	})
+}
+
+// Start 启动心跳检测
+func (manager *ConnectionManager) Start() {
+	defer manager.ticker.Stop()
+	for {
+		select {
+		case <-manager.ticker.C:
+			manager.CheckAll()
+		case <-manager.stopChan:
+			return
+		}
+	}
+}
+
+// Stop 停止心跳检测
+func (manager *ConnectionManager) Stop() {
+	close(manager.stopChan)
+}