Przeglądaj źródła

Merge remote-tracking branch 'origin/feature/deepseek_rag_1.0' into debug

# Conflicts:
#	main.go
Roc 1 miesiąc temu
rodzic
commit
8e3a4f76b9

+ 228 - 0
controllers/rag/chat_controller.go

@@ -0,0 +1,228 @@
+package rag
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/models/llm"
+	"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 ChatController struct {
+	controllers.BaseAuthController
+}
+
+func (cc *ChatController) 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
+	}
+}
+
+// NewChat @Title 新建对话框
+// @Description 新建对话框
+// @Success 101 {object} response.ListResp
+// @router /chat/new_chat [post]
+func (kbctrl *KbController) NewChat() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		kbctrl.Data["json"] = br
+		kbctrl.ServeJSON()
+	}()
+	var req facade.LLMKnowledgeSearch
+	err := json.Unmarshal(kbctrl.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	sysUser := kbctrl.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	session := llm.UserLlmChat{
+		UserId:      sysUser.AdminId,
+		CreatedTime: time.Now(),
+		ChatTitle:   "新会话",
+	}
+	err = session.CreateChatSession()
+	if err != nil {
+		br.Msg = "创建失败"
+		br.ErrMsg = "创建失败,Err:" + err.Error()
+		return
+	}
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "创建成功"
+}
+
+// ChatConnect @Title 知识库问答创建对话连接
+// @Description 知识库问答创建对话连接
+// @Success 101 {object} response.ListResp
+// @router /chat/connect [get]
+func (cc *ChatController) 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
+}

+ 52 - 0
controllers/rag/kb_controller.go

@@ -0,0 +1,52 @@
+package rag
+
+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 = "获取成功"
+}
+

+ 11 - 0
controllers/rag/llm_http/request.go

@@ -0,0 +1,11 @@
+package llm_http
+
+type LLMQuestionReq struct {
+	Question      string `description:"提问"`
+	KnowledgeBase string `description:"知识库"`
+	SessionId     string `description:"会话ID"`
+}
+
+type CreateChatReq struct {
+	ChatTitle string `json:"ChatTitle" description:"会话名称"`
+}

+ 7 - 0
controllers/rag/llm_http/response.go

@@ -0,0 +1,7 @@
+package llm_http
+
+
+type LLMQuestionRes struct {
+	Answer      string `description:"回答"`
+	SessionId     string `description:"会话ID"`
+}

+ 233 - 0
controllers/rag/question.go

@@ -0,0 +1,233 @@
+package rag
+
+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.WechatPlatform
+// @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 = ?`, 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 = `添加成功`
+}
+
+// Edit
+// @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 = `删除成功`
+}

+ 600 - 0
controllers/rag/wechat_platform_controller.go

@@ -0,0 +1,600 @@
+package rag
+
+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/models/system"
+	"eta/eta_api/services/llm"
+	"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 = ?`, 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
+	}
+
+	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 llm.AddWechatPlatform(item)
+
+	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 = ?`, rag.WechatPlatformColumns.Nickname)
+		pars = append(pars, `%`+keyWord+`%`)
+	}
+
+	condition = fmt.Sprintf(` AND b.%s = ?`, rag.WechatPlatformUserMappingColumns.SysUserID)
+	pars = append(pars, c.SysUser.AdminId)
+
+	if keyWord != "" {
+		condition = fmt.Sprintf(` AND %s = ?`, rag.WechatPlatformColumns.Nickname)
+		pars = append(pars, `%`+keyWord+`%`)
+	}
+
+	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 = ?`, 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
+	}
+
+	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 %s = ?`, rag.WechatPlatformColumns.Nickname)
+		pars = append(pars, `%`+keyWord+`%`)
+	}
+
+	if wechatPlatformId > 0 {
+		condition = fmt.Sprintf(` AND %s = ?`, rag.WechatArticleColumns.WechatPlatformID)
+		pars = append(pars, wechatPlatformId)
+	}
+
+	obj := new(rag.WechatArticle)
+	total, list, err := obj.GetPageListByCondition(condition, pars, startSize, pageSize)
+	if err != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取失败,Err:" + err.Error()
+		return
+	}
+
+	viewList := make([]rag.WechatArticleView, 0)
+	if list != nil && len(list) > 0 {
+		viewList = obj.ListToViewList(list)
+
+		wechatPlatformIdList := make([]int, 0)
+		wechatPlatformIdMap := make(map[int]bool)
+		for _, v := range list {
+			if _, ok := wechatPlatformIdMap[v.WechatPlatformId]; ok {
+				continue
+			}
+			wechatPlatformIdList = append(wechatPlatformIdList, v.WechatPlatformId)
+			wechatPlatformIdMap[v.WechatPlatformId] = true
+		}
+
+		wechatPlatformMap := make(map[int]*rag.WechatPlatform)
+		if len(wechatPlatformIdList) > 0 {
+			var wechatArticleCondition string
+			var wechatArticlePars []interface{}
+			wechatArticleCondition = fmt.Sprintf(` AND %s in (?)`, rag.WechatArticleColumns.WechatPlatformID)
+			wechatArticlePars = append(wechatArticlePars, wechatPlatformIdList)
+			wechatPlatformObj := new(rag.WechatPlatform)
+			wechatPlatformList, err := wechatPlatformObj.GetListByCondition(wechatArticleCondition, wechatArticlePars, startSize, 100000)
+			if err != nil {
+				br.Msg = "获取失败"
+				br.ErrMsg = "获取失败,Err:" + err.Error()
+				return
+			}
+			for _, v := range wechatPlatformList {
+				wechatPlatformMap[v.WechatPlatformId] = v
+			}
+		}
+
+		for k, v := range viewList {
+			wechatPlatformInfo, ok := wechatPlatformMap[v.WechatPlatformId]
+			if !ok {
+				continue
+			}
+			viewList[k].WechatPlatformName = wechatPlatformInfo.Nickname
+			viewList[k].WechatPlatformRoundHeadImg = wechatPlatformInfo.RoundHeadImg
+		}
+	}
+	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
+	}
+	item.Content = html.UnescapeString(item.Content)
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = item
+}
+
+// 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
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "删除成功"
+}

+ 0 - 1
main.go

@@ -23,7 +23,6 @@ func main() {
 		web.BConfig.WebConfig.DirectoryIndex = true
 		web.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
 	}
-
 	go services.Task()
 
 	// 异常处理

+ 25 - 0
models/llm/user_llm_chat.go

@@ -0,0 +1,25 @@
+package llm
+
+import (
+	"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:更新时间"`
+}
+
+func (u *UserLlmChat) TableName() string {
+	return "user_llm_chat"
+}
+func (u *UserLlmChat) CreateChatSession() (err error) {
+	o := global.DbMap[utils.DbNameAI]
+	err = o.Create(u).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 sort asc,question_id asc 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
+}

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

@@ -0,0 +1,21 @@
+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 AddQuestionReq struct {
+	Content string `description:"公众号名称"`
+}
+
+type EditQuestionReq struct {
+	QuestionId int    `description:"问题id"`
+	Content    string `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
+}

+ 196 - 0
models/rag/wechat_article.go

@@ -0,0 +1,196 @@
+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:"文本内容"`
+	Abstract          string    `gorm:"column:abstract;type:text;comment:摘要;" description:"摘要"`
+	Country           string    `gorm:"column:country;type:varchar(255);comment:国家;" json:"country"`  // 国家
+	Province          string    `gorm:"column:province;type:varchar(255);comment:省;" json:"province"` // 省
+	City              string    `gorm:"column:city;type:varchar(255);comment:市;" json:"city"`         // 市
+	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:"入库时间"`
+}
+
+// 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
+	Abstract          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",
+	Abstract:          "abstract",
+	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:"文本内容"`
+	Abstract                   string `gorm:"column:abstract;type:text;comment:摘要;" description:"摘要"`
+	Country                    string `gorm:"column:country;type:varchar(255);comment:国家;" json:"country"`  // 国家
+	Province                   string `gorm:"column:province;type:varchar(255);comment:省;" json:"province"` // 省
+	City                       string `gorm:"column:city;type:varchar(255);comment:市;" json:"city"`         // 市
+	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,
+		Abstract:                   m.Abstract,
+		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
+}

+ 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
+}

+ 86 - 0
models/rag/wechat_platform_user_mapping.go

@@ -0,0 +1,86 @@
+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
+}

+ 136 - 2
routers/commentsRouter.go

@@ -8557,6 +8557,141 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_api/controllers/rag:ChatController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag:ChatController"],
+        beego.ControllerComments{
+            Method: "ChatConnect",
+            Router: `/chat/connect`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/rag:KbController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag:KbController"],
+        beego.ControllerComments{
+            Method: "NewChat",
+            Router: `/chat/new_chat`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/rag:KbController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag: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/rag:QuestionController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag:QuestionController"],
+        beego.ControllerComments{
+            Method: "Add",
+            Router: `/question/add`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/rag:QuestionController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag:QuestionController"],
+        beego.ControllerComments{
+            Method: "Del",
+            Router: `/question/del`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/rag:QuestionController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag:QuestionController"],
+        beego.ControllerComments{
+            Method: "Edit",
+            Router: `/question/edit`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/rag:QuestionController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag:QuestionController"],
+        beego.ControllerComments{
+            Method: "List",
+            Router: `/question/list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/rag:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag:WechatPlatformController"],
+        beego.ControllerComments{
+            Method: "TagList",
+            Router: `/tag/list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/rag:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag: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/rag:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag: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/rag:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag: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/rag:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag: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/rag:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag: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/rag:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag: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/rag:WechatPlatformController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/rag: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/report_approve:ReportApproveController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/report_approve:ReportApproveController"],
         beego.ControllerComments{
             Method: "Approve",
@@ -13146,5 +13281,4 @@ func init() {
             MethodParams: param.Make(),
             Filters: nil,
             Params: nil})
-
-}
+}

+ 11 - 0
routers/router.go

@@ -31,6 +31,7 @@ import (
 	"eta/eta_api/controllers/eta_trial"
 	"eta/eta_api/controllers/fe_calendar"
 	"eta/eta_api/controllers/material"
+	"eta/eta_api/controllers/rag"
 	"eta/eta_api/controllers/report_approve"
 	"eta/eta_api/controllers/residual_analysis"
 	"eta/eta_api/controllers/roadshow"
@@ -69,6 +70,16 @@ func init() {
 				&controllers.ClassifyController{},
 			),
 		),
+		web.NSNamespace("/llm",
+			web.NSInclude(
+				&rag.ChatController{},
+				&rag.WechatPlatformController{},
+				&rag.QuestionController{},
+			),
+			web.NSInclude(
+				&rag.KbController{},
+			),
+		),
 		web.NSNamespace("/banner",
 			web.NSInclude(
 				&controllers.BannerController{},

+ 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
+}

+ 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"`
+}

+ 188 - 0
services/llm/wechat_platform.go

@@ -0,0 +1,188 @@
+package llm
+
+import (
+	"eta/eta_api/models/rag"
+	"eta/eta_api/utils"
+	"fmt"
+	"html"
+	"strconv"
+	"time"
+)
+
+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 := 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
+		}
+	}
+
+	// 把刚搜索的文章加入到指标库
+	AddWechatArticle(item, articleLink, articleDetail, nil)
+
+	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 WechatArticleDataResp, articleMenu *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)
+	}
+
+	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(articleDetail.HtmlContent),
+		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.Abstract = articleMenu.Digest
+	}
+	err = obj.Create()
+}
+
+// BeachAddWechatPlatform
+// @Description: 批量添加公众号文章
+// @author: Roc
+// @datetime 2025-03-05 15:05:07
+// @param item *rag.WechatPlatform
+// @return err error
+func BeachAddWechatPlatform(item *rag.WechatPlatform) (err error) {
+	defer func() {
+		fmt.Println("公众号文章批量入库完成")
+		if err != nil {
+			utils.FileLog.Error("公众号文章批量入库失败,err:%v", err)
+			fmt.Println("公众号文章批量入库失败,err:", err)
+		}
+	}()
+	if item.FakeId == `` {
+		return
+	}
+
+	num := 10
+
+	// 获取公众号的文章列表
+	articleListResp, err := SearchByWechatArticleList(item.FakeId, num)
+	if err != nil {
+		return
+	}
+	for _, articleMenu := range articleListResp.List {
+		articleDetail, tmpErr := SearchByWechatArticle(articleMenu.Link)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+
+		// 把刚搜索的文章加入到指标库
+		AddWechatArticle(item, articleMenu.Link, articleDetail, &articleMenu)
+
+		time.Sleep(10 * time.Second)
+	}
+	return
+}

+ 1 - 1
services/task.go

@@ -60,7 +60,7 @@ func Task() {
 		// 监听数据源binlog写入es
 		go binlog.HandleDataSourceChange2Es()
 	}
-
+	go StartSessionManager()
 	// TODO:数据修复
 	//FixNewEs()
 	fmt.Println("task end")

+ 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()
+}

+ 18 - 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 //数据库连接
@@ -209,6 +214,10 @@ var (
 	ETA_FORUM_HUB_MD5_KEY string
 )
 
+var (
+	ETA_WX_CRAWLER_URL string
+)
+
 // BusinessCode 商家编码
 var BusinessCode string
 
@@ -577,6 +586,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 小程序桥接服务地址
@@ -629,6 +644,9 @@ func init() {
 	// chrome配置
 	ChromePath = config["chrome_path"]
 
+	//模型服务器地址
+	LLM_SERVER = config["llm_server"]
+	LLM_MODEL = config["llm_model"]
 	// 初始化ES
 	initEs()
 

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

@@ -0,0 +1,193 @@
+package eta_llm
+
+import (
+	"bufio"
+	"bytes"
+	"encoding/json"
+	"errors"
+	"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"
+	KNOWLEDGE_BASE_SEARCH_DOCS_API = "/knowledge_base/search_docs"
+)
+
+type ETALLMClient struct {
+	*llm.LLMClient
+	LlmModel string
+}
+
+func GetInstance() llm.LLMService {
+	dsOnce.Do(func() {
+		if etaLlmClient == nil {
+			etaLlmClient = &ETALLMClient{
+				LLMClient: llm.NewLLMClient(utils.LLM_SERVER, 120),
+				LlmModel:  utils.LLM_MODEL,
+			}
+		}
+	})
+	return etaLlmClient
+}
+
+func (ds *ETALLMClient) KnowledgeBaseChat(query string, KnowledgeBaseName string, history []string) (llmRes *http.Response, err error) {
+	ChatHistory := make([]eta_llm_http.HistoryContent, 0)
+	for _, historyItemStr := range history {
+		str := strings.Split(historyItemStr, "-")
+		historyItem := eta_llm_http.HistoryContent{
+			Role:    str[0],
+			Content: str[1],
+		}
+		ChatHistory = append(ChatHistory, historyItem)
+	}
+	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
+}

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

@@ -0,0 +1,30 @@
+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 HistoryContent 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"`
+}

+ 25 - 0
utils/llm/llm_client.go

@@ -0,0 +1,25 @@
+package llm
+
+import (
+	"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 []string) (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
+}

+ 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
+}

+ 93 - 0
utils/ws/limiter.go

@@ -0,0 +1,93 @@
+package ws
+
+import (
+	"fmt"
+	"golang.org/x/time/rate"
+	"sync"
+	"time"
+)
+
+var (
+	limiterManagers map[string]*LimiterManger
+	limiterOnce     sync.Once
+	limters         = map[string]string{
+		CONNECT_LIMITER: LIMITER_KEY,
+		QA_LIMITER:      CONNECT_LIMITER_KEY,
+	}
+)
+
+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	=30*time.Second
+)
+
+type RateLimiter struct {
+	LastRequest time.Time
+	*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) *RateLimiter {
+	qalm.Lock()
+	defer qalm.Unlock()
+	if limiter, exists := qalm.limiterMap[token]; exists {
+		return limiter
+	}
+
+	// 创建一个新的限流器,例如每10秒1个请求
+	limiter := &RateLimiter{
+		Limiter: rate.NewLimiter(rate.Every(RATE_LIMTER_TIME), 1),
+	}
+	qalm.limiterMap[token] = limiter
+	return limiter
+}
+func (qalm *LimiterManger) Allow(token string) bool {
+	limiter := qalm.GetLimiter(token)
+	if limiter.LastRequest.IsZero() {
+		limiter.LastRequest = time.Now()
+		return limiter.Allow()
+	}
+	if time.Now().Sub(limiter.LastRequest) < RATE_LIMTER_TIME {
+		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(limters))
+		}
+		for key, _ := range limters {
+			limiterManagers[key] = &LimiterManger{
+				limiterMap: make(map[string]*RateLimiter),
+			}
+		}
+	})
+	return limiterManagers[key]
+}
+
+func Allow(userId int, limiter string) bool {
+	tokenKey := limters[limiter]
+	if tokenKey == "" {
+		return false
+	}
+	token := fmt.Sprintf(tokenKey, userId)
+	handler := getInstance(limiter)
+	if handler == nil {
+		return false
+	}
+	return handler.Allow(token)
+}

+ 159 - 0
utils/ws/session.go

@@ -0,0 +1,159 @@
+package ws
+
+import (
+	"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     []string
+	CloseChan   chan struct{}
+	MessageChan chan string
+	mu          sync.RWMutex
+	sessionOnce sync.Once
+}
+type Message struct {
+	KbName     string   `json:"KbName"`
+	Query      string   `json:"Query"`
+	LastTopics []string `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(err.Error())
+
+		}
+	}
+}
+
+// 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 {
+		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) {
+	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
+}

+ 179 - 0
utils/ws/session_manager.go

@@ -0,0 +1,179 @@
+package ws
+
+import (
+	"encoding/json"
+	"errors"
+	"eta/eta_api/utils"
+	"eta/eta_api/utils/llm"
+	"eta/eta_api/utils/llm/eta_llm"
+	"fmt"
+	"github.com/gorilla/websocket"
+	"net/http"
+	"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 {
+	if !Allow(userID, QA_LIMITER) {
+		return errors.New("您提问的太频繁了,请稍后再试")
+	}
+	session, exists := manager.GetSession(sessionID)
+	if !exists {
+		return errors.New("session not found")
+	}
+	var userMessage Message
+	err := json.Unmarshal(message, &userMessage)
+	if err != nil {
+		return errors.New("消息格式错误")
+	}
+	// 处理业务逻辑
+	session.History = append(session.History, userMessage.LastTopics...)
+	resp, err := llmService.KnowledgeBaseChat(userMessage.Query, userMessage.KbName, session.History)
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+	if err != nil {
+		err = errors.New(fmt.Sprintf("知识库问答失败: httpCode:%d,错误信息:%s", resp.StatusCode, http.StatusText(resp.StatusCode)))
+		return err
+	}
+	if resp.StatusCode != http.StatusOK {
+		err = errors.New(fmt.Sprintf("知识库问答失败: httpCode:%d,错误信息:%s", resp.StatusCode, http.StatusText(resp.StatusCode)))
+		return err
+	}
+	// 解析流式响应
+	contentChan, errChan, closeChan := eta_llm.ParseStreamResponse(resp)
+	// 处理流式数据并发送到 WebSocket
+	for {
+		select {
+		case content, ok := <-contentChan:
+			if !ok {
+				err = errors.New("未知的错误异常")
+				return err
+			}
+			session.UpdateActivity()
+			// 发送消息到 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:
+			_ = 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)
+}