init push
This commit is contained in:
246
backend/handler/share/app.go
Normal file
246
backend/handler/share/app.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
wechat_v2 "github.com/silenceper/wechat/v2"
|
||||
"github.com/silenceper/wechat/v2/cache"
|
||||
offConfig "github.com/silenceper/wechat/v2/officialaccount/config"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/message"
|
||||
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type ShareAppHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
usecase *usecase.AppUsecase
|
||||
}
|
||||
|
||||
func NewShareAppHandler(
|
||||
e *echo.Echo,
|
||||
baseHandler *handler.BaseHandler,
|
||||
logger *log.Logger,
|
||||
usecase *usecase.AppUsecase,
|
||||
) *ShareAppHandler {
|
||||
h := &ShareAppHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.share.app"),
|
||||
usecase: usecase,
|
||||
}
|
||||
|
||||
share := e.Group("share/v1/app",
|
||||
func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Response().Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
c.Response().Header().Set("Access-Control-Allow-Headers", "Content-Type, Origin, Accept")
|
||||
if c.Request().Method == "OPTIONS" {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
share.GET("/web/info", h.GetWebAppInfo)
|
||||
share.GET("/widget/info", h.GetWidgetAppInfo)
|
||||
share.GET("/wechat/info", h.WechatAppInfo)
|
||||
|
||||
// wechat official account
|
||||
share.GET("/wechat/official_account", h.VerifyUrlWechatOfficialAccount)
|
||||
share.POST("/wechat/official_account", h.WechatHandlerOfficialAccount)
|
||||
return h
|
||||
}
|
||||
|
||||
// GetWebAppInfo
|
||||
//
|
||||
// @Summary GetAppInfo
|
||||
// @Description GetAppInfo
|
||||
// @Tags share_app
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb id"
|
||||
// @Success 200 {object} domain.Response{data=domain.AppInfoResp}
|
||||
// @Router /share/v1/app/web/info [get]
|
||||
func (h *ShareAppHandler) GetWebAppInfo(c echo.Context) error {
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
ctx := context.WithValue(c.Request().Context(), consts.ContextKeyEdition, consts.GetLicenseEdition(c))
|
||||
appInfo, err := h.usecase.ShareGetWebAppInfo(ctx, kbID, domain.GetAuthID(c))
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, err.Error(), err)
|
||||
}
|
||||
return h.NewResponseWithData(c, appInfo)
|
||||
}
|
||||
|
||||
// GetWidgetAppInfo
|
||||
//
|
||||
// @Summary GetWidgetAppInfo
|
||||
// @Description GetWidgetAppInfo
|
||||
// @Tags share_app
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb id"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /share/v1/app/widget/info [get]
|
||||
func (h *ShareAppHandler) GetWidgetAppInfo(c echo.Context) error {
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
appInfo, err := h.usecase.GetWidgetAppInfo(c.Request().Context(), kbID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, err.Error(), err)
|
||||
}
|
||||
return h.NewResponseWithData(c, appInfo)
|
||||
}
|
||||
|
||||
// WechatAppInfo
|
||||
//
|
||||
// @Summary WechatAppInfo
|
||||
// @Description WechatAppInfo
|
||||
// @Tags share_chat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb id"
|
||||
// @Success 200 {object} domain.Response{data=v1.WechatAppInfoResp}
|
||||
// @Router /share/v1/app/wechat/info [get]
|
||||
func (h *ShareAppHandler) WechatAppInfo(c echo.Context) error {
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
appInfo, err := h.usecase.GetWechatAppInfo(c.Request().Context(), kbID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, err.Error(), err)
|
||||
}
|
||||
return h.NewResponseWithData(c, appInfo)
|
||||
}
|
||||
|
||||
func (h *ShareAppHandler) VerifyUrlWechatOfficialAccount(c echo.Context) error {
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// get wechat official account info
|
||||
appInfo, err := h.usecase.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppTypeWechatOfficialAccount)
|
||||
if err != nil {
|
||||
h.logger.Error("get app detail failed")
|
||||
return h.NewResponseWithError(c, "GetAppDetailByKBIDAndAppType failed", err)
|
||||
}
|
||||
|
||||
if appInfo.Settings.WechatOfficialAccountIsEnabled != nil && !*appInfo.Settings.WechatOfficialAccountIsEnabled {
|
||||
return h.NewResponseWithError(c, "wechat official account is not enabled", err)
|
||||
}
|
||||
wc := wechat_v2.NewWechat()
|
||||
memory := cache.NewMemory()
|
||||
cfg := &offConfig.Config{
|
||||
AppID: appInfo.Settings.WechatOfficialAccountAppID,
|
||||
AppSecret: appInfo.Settings.WechatOfficialAccountAppSecret,
|
||||
Token: appInfo.Settings.WechatOfficialAccountToken,
|
||||
EncodingAESKey: appInfo.Settings.WechatOfficialAccountEncodingAESKey,
|
||||
Cache: memory,
|
||||
}
|
||||
officialAccount := wc.GetOfficialAccount(cfg)
|
||||
server := officialAccount.GetServer(c.Request(), c.Response().Writer)
|
||||
|
||||
// success
|
||||
err = server.Serve()
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "serve message failed", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ShareAppHandler) WechatHandlerOfficialAccount(c echo.Context) error {
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// get wechat official account info
|
||||
appInfo, err := h.usecase.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppTypeWechatOfficialAccount)
|
||||
if err != nil {
|
||||
h.logger.Error("get app detail failed")
|
||||
return h.NewResponseWithError(c, "GetAppDetailByKBIDAndAppType failed", err)
|
||||
}
|
||||
|
||||
if appInfo.Settings.WechatOfficialAccountIsEnabled != nil && !*appInfo.Settings.WechatOfficialAccountIsEnabled {
|
||||
return h.NewResponseWithError(c, "wechat official account is not enabled", err)
|
||||
}
|
||||
wc := wechat_v2.NewWechat()
|
||||
memory := cache.NewMemory()
|
||||
cfg := &offConfig.Config{
|
||||
AppID: appInfo.Settings.WechatOfficialAccountAppID,
|
||||
AppSecret: appInfo.Settings.WechatOfficialAccountAppSecret,
|
||||
Token: appInfo.Settings.WechatOfficialAccountToken,
|
||||
EncodingAESKey: appInfo.Settings.WechatOfficialAccountEncodingAESKey,
|
||||
Cache: memory,
|
||||
}
|
||||
officialAccount := wc.GetOfficialAccount(cfg)
|
||||
server := officialAccount.GetServer(c.Request(), c.Response().Writer)
|
||||
|
||||
// message handler
|
||||
server.SetMessageHandler(func(msg *message.MixMessage) *message.Reply {
|
||||
h.logger.Info("received message:", log.Any("msgtype", msg.MsgType), log.Any("fromUserName", msg.FromUserName), log.String("content", msg.Content), log.Any("event type", msg.Event))
|
||||
|
||||
switch msg.MsgType {
|
||||
case message.MsgTypeText:
|
||||
// text消息
|
||||
userOpenID := msg.FromUserName
|
||||
userContent := msg.Content
|
||||
h.logger.Info("user_open_id user_content", log.Any("user_open_id", userOpenID), log.Any("user content", userContent))
|
||||
// 异步发送
|
||||
go func(openID, content string) {
|
||||
ctx := context.Background()
|
||||
// send content to ai
|
||||
result, err := h.usecase.GetWechatOfficialAccountResponse(ctx, officialAccount, kbID, openID, content)
|
||||
if err != nil {
|
||||
h.logger.Error("get wechat official account response failed", log.Error(err))
|
||||
return
|
||||
}
|
||||
// send response to user --> 需要开启客服消息权限
|
||||
err = h.usecase.SendCustomerServiceMessage(officialAccount, string(userOpenID), result)
|
||||
if err != nil {
|
||||
h.logger.Error("send to customer service failed", log.Error(err))
|
||||
}
|
||||
}(string(userOpenID), userContent)
|
||||
return &message.Reply{MsgType: message.MsgTypeText, MsgData: message.NewText("您的问题已经收到,正在努力思考中,请稍候...")}
|
||||
case message.MsgTypeEvent:
|
||||
if msg.Event == message.EventSubscribe {
|
||||
return &message.Reply{MsgType: message.MsgTypeText, MsgData: message.NewText("感谢关注,欢迎提问!")} // 立即回复简单信息
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
h.logger.Info("unknown message type", log.Any("message type", msg.MsgType))
|
||||
return &message.Reply{MsgType: message.MsgTypeText, MsgData: message.NewText("未知消息类型,请发送正确的类型...")}
|
||||
}
|
||||
})
|
||||
|
||||
// success
|
||||
err = server.Serve()
|
||||
if err != nil {
|
||||
h.logger.Error("serve message failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "serve message failed", err)
|
||||
}
|
||||
|
||||
// send message to user
|
||||
err = server.Send()
|
||||
if err != nil {
|
||||
h.logger.Error("send message failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "send message failed", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
183
backend/handler/share/auth.go
Normal file
183
backend/handler/share/auth.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/share/v1"
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/middleware"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type ShareAuthHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
kbUsecase *usecase.KnowledgeBaseUsecase
|
||||
authUsecase *usecase.AuthUsecase
|
||||
}
|
||||
|
||||
func NewShareAuthHandler(
|
||||
e *echo.Echo,
|
||||
baseHandler *handler.BaseHandler,
|
||||
logger *log.Logger,
|
||||
kbUsecase *usecase.KnowledgeBaseUsecase,
|
||||
authUsecase *usecase.AuthUsecase,
|
||||
) *ShareAuthHandler {
|
||||
h := &ShareAuthHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.share.auth"),
|
||||
kbUsecase: kbUsecase,
|
||||
authUsecase: authUsecase,
|
||||
}
|
||||
|
||||
shareAuthMiddleware := middleware.NewShareAuthMiddleware(logger, kbUsecase)
|
||||
|
||||
share := e.Group("share/v1/auth", shareAuthMiddleware.CheckForbidden)
|
||||
share.GET("/get", h.AuthGet)
|
||||
share.POST("/login/simple", h.AuthLoginSimple)
|
||||
share.POST("/github", h.AuthGitHub)
|
||||
return h
|
||||
}
|
||||
|
||||
// AuthGet auth获取
|
||||
//
|
||||
// @Tags share_auth
|
||||
// @Summary AuthGet
|
||||
// @Description AuthGet
|
||||
// @ID v1-AuthGet
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb_id"
|
||||
// @Param param query v1.AuthGetReq true "para"
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.AuthGetResp}
|
||||
// @Router /share/v1/auth/get [get]
|
||||
func (h *ShareAuthHandler) AuthGet(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
kb, err := h.kbUsecase.GetKnowledgeBase(ctx, kbID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get knowledge base detail", err)
|
||||
}
|
||||
|
||||
resp := &v1.AuthGetResp{
|
||||
AuthType: kb.AccessSettings.GetAuthType(),
|
||||
SourceType: kb.AccessSettings.SourceType,
|
||||
LicenseEdition: consts.GetLicenseEdition(c),
|
||||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
||||
// AuthLoginSimple 简单口令登录
|
||||
//
|
||||
// @Tags share_auth
|
||||
// @Summary AuthLoginSimple
|
||||
// @Description AuthLoginSimple
|
||||
// @ID v1-AuthLoginSimple
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb_id"
|
||||
// @Param param body v1.AuthLoginSimpleReq true "para"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /share/v1/auth/login/simple [post]
|
||||
func (h *ShareAuthHandler) AuthLoginSimple(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
var req v1.AuthLoginSimpleReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Error("parse request failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "AuthGet bind failed", nil)
|
||||
}
|
||||
|
||||
kb, err := h.kbUsecase.GetKnowledgeBase(ctx, kbID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get knowledge base detail", err)
|
||||
}
|
||||
|
||||
if !kb.AccessSettings.SimpleAuth.Enabled {
|
||||
return h.NewResponseWithError(c, "simple auth is not enabled", nil)
|
||||
}
|
||||
|
||||
if req.Password != kb.AccessSettings.SimpleAuth.Password {
|
||||
return h.NewResponseWithError(c, "simple auth password is incorrect", nil)
|
||||
}
|
||||
|
||||
s := c.Get(domain.SessionCacheKey)
|
||||
if s == nil {
|
||||
return h.NewResponseWithError(c, "get session cache key failed", nil)
|
||||
}
|
||||
store := s.(sessions.Store)
|
||||
|
||||
newSess := sessions.NewSession(store, domain.SessionName)
|
||||
newSess.IsNew = true
|
||||
|
||||
newSess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 86400 * 30,
|
||||
HttpOnly: true,
|
||||
}
|
||||
|
||||
newSess.Values["kb_id"] = kb.ID
|
||||
|
||||
if err := newSess.Save(c.Request(), c.Response()); err != nil {
|
||||
return h.NewResponseWithError(c, "save session failed", nil)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// AuthGitHub GitHub登录
|
||||
//
|
||||
// @Tags ShareAuth
|
||||
// @Summary GitHub登录
|
||||
// @Description GitHub登录
|
||||
// @ID v1-AuthGitHub
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb id"
|
||||
// @Param param body v1.AuthGitHubReq true "para"
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.AuthGitHubResp}
|
||||
// @Router /share/v1/auth/github [post]
|
||||
func (h *ShareAuthHandler) AuthGitHub(c echo.Context) error {
|
||||
ctx := context.WithValue(c.Request().Context(), consts.ContextKeyEdition, consts.GetLicenseEdition(c))
|
||||
|
||||
var req v1.AuthGitHubReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
req.KbID = kbID
|
||||
|
||||
valid, err := h.authUsecase.ValidateRedirectUrl(ctx, req.KbID, req.RedirectUrl)
|
||||
if err != nil || !valid {
|
||||
return h.NewResponseWithError(c, "invalid redirect url", err)
|
||||
}
|
||||
|
||||
url, err := h.authUsecase.GenerateGitHubAuthUrl(ctx, req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "GenerateGitHubAuthUrl failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, v1.AuthGitHubResp{
|
||||
Url: url,
|
||||
})
|
||||
}
|
||||
91
backend/handler/share/captcha.go
Normal file
91
backend/handler/share/captcha.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
gocap "github.com/ackcoder/go-cap"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
)
|
||||
|
||||
type ShareCaptchaHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewShareCaptchaHandler(
|
||||
baseHandler *handler.BaseHandler,
|
||||
echo *echo.Echo,
|
||||
logger *log.Logger,
|
||||
) *ShareCaptchaHandler {
|
||||
h := &ShareCaptchaHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.share.captcha"),
|
||||
}
|
||||
|
||||
group := echo.Group("share/v1/captcha")
|
||||
group.POST("/challenge", h.CreateCaptcha)
|
||||
group.POST("/redeem", h.RedeemCaptcha)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// CreateCaptcha
|
||||
//
|
||||
// @Summary CreateCaptcha
|
||||
// @Description CreateCaptcha
|
||||
// @Tags share_captcha
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb id"
|
||||
// @Success 200 {object} gocap.ChallengeData
|
||||
// @Router /share/v1/captcha/challenge [post]
|
||||
func (h *ShareCaptchaHandler) CreateCaptcha(c echo.Context) error {
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
data, err := h.Captcha.CreateChallenge(c.Request().Context())
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "create captcha failed", err)
|
||||
}
|
||||
return c.JSON(http.StatusCreated, data)
|
||||
}
|
||||
|
||||
// RedeemCaptcha
|
||||
//
|
||||
// @Summary RedeemCaptcha
|
||||
// @Description RedeemCaptcha
|
||||
// @Tags share_captcha
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb id"
|
||||
// @Param body body consts.RedeemCaptchaReq true "request"
|
||||
// @Success 200 {object} gocap.VerificationResult
|
||||
// @Router /share/v1/captcha/redeem [post]
|
||||
func (h *ShareCaptchaHandler) RedeemCaptcha(c echo.Context) error {
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
var req consts.RedeemCaptchaReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request is invalid", err)
|
||||
}
|
||||
data, err := h.Captcha.RedeemChallenge(c.Request().Context(), req.Token, req.Solutions)
|
||||
if err != nil {
|
||||
sentry.CaptureException(err)
|
||||
return c.JSON(http.StatusInternalServerError, gocap.VerificationResult{
|
||||
Success: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.JSON(http.StatusCreated, gocap.VerificationResult{
|
||||
Success: true,
|
||||
TokenData: data,
|
||||
})
|
||||
}
|
||||
550
backend/handler/share/chat.go
Normal file
550
backend/handler/share/chat.go
Normal file
@@ -0,0 +1,550 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type ShareChatHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
appUsecase *usecase.AppUsecase
|
||||
chatUsecase *usecase.ChatUsecase
|
||||
authUsecase *usecase.AuthUsecase
|
||||
conversationUsecase *usecase.ConversationUsecase
|
||||
modelUsecase *usecase.ModelUsecase
|
||||
}
|
||||
|
||||
func NewShareChatHandler(
|
||||
e *echo.Echo,
|
||||
baseHandler *handler.BaseHandler,
|
||||
logger *log.Logger,
|
||||
appUsecase *usecase.AppUsecase,
|
||||
chatUsecase *usecase.ChatUsecase,
|
||||
authUsecase *usecase.AuthUsecase,
|
||||
conversationUsecase *usecase.ConversationUsecase,
|
||||
modelUsecase *usecase.ModelUsecase,
|
||||
) *ShareChatHandler {
|
||||
h := &ShareChatHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.share.chat"),
|
||||
appUsecase: appUsecase,
|
||||
chatUsecase: chatUsecase,
|
||||
authUsecase: authUsecase,
|
||||
conversationUsecase: conversationUsecase,
|
||||
modelUsecase: modelUsecase,
|
||||
}
|
||||
|
||||
share := e.Group("share/v1/chat",
|
||||
func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Response().Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
c.Response().Header().Set("Access-Control-Allow-Headers", "Content-Type, Origin, Accept")
|
||||
if c.Request().Method == "OPTIONS" {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
share.POST("/message", h.ChatMessage, h.ShareAuthMiddleware.Authorize)
|
||||
share.POST("/search", h.ChatSearch, h.ShareAuthMiddleware.Authorize)
|
||||
share.POST("/completions", h.ChatCompletions)
|
||||
share.POST("/widget", h.ChatWidget)
|
||||
share.POST("/widget/search", h.WidgetSearch)
|
||||
share.POST("/feedback", h.FeedBack)
|
||||
return h
|
||||
}
|
||||
|
||||
// ChatMessage chat message
|
||||
//
|
||||
// @Summary ChatMessage
|
||||
// @Description ChatMessage
|
||||
// @Tags share_chat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param app_type query string true "app type"
|
||||
// @Param request body domain.ChatRequest true "request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /share/v1/chat/message [post]
|
||||
func (h *ShareChatHandler) ChatMessage(c echo.Context) error {
|
||||
var req domain.ChatRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Error("parse request failed", log.Error(err))
|
||||
return h.sendErrMsg(c, "parse request failed")
|
||||
}
|
||||
req.KBID = c.Request().Header.Get("X-KB-ID") // get from caddy header
|
||||
if err := c.Validate(&req); err != nil {
|
||||
h.logger.Error("validate request failed", log.Error(err))
|
||||
return h.sendErrMsg(c, "validate request failed")
|
||||
}
|
||||
|
||||
for _, path := range req.ImagePaths {
|
||||
if !strings.HasPrefix(path, "/static-file/") {
|
||||
return h.sendErrMsg(c, "invalid image path")
|
||||
}
|
||||
}
|
||||
|
||||
if req.Message == "" && len(req.ImagePaths) == 0 {
|
||||
return h.sendErrMsg(c, "message is empty")
|
||||
}
|
||||
|
||||
if req.AppType != domain.AppTypeWeb {
|
||||
return h.sendErrMsg(c, "invalid app type")
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
// validate captcha token
|
||||
if !h.Captcha.ValidateToken(ctx, req.CaptchaToken) {
|
||||
return h.sendErrMsg(c, "failed to validate captcha")
|
||||
}
|
||||
|
||||
req.RemoteIP = c.RealIP()
|
||||
|
||||
c.Response().Header().Set("Content-Type", "text/event-stream")
|
||||
c.Response().Header().Set("Cache-Control", "no-cache")
|
||||
c.Response().Header().Set("Connection", "keep-alive")
|
||||
c.Response().Header().Set("Transfer-Encoding", "chunked")
|
||||
|
||||
// get user info --> no enterprise is nil
|
||||
userID := c.Get("user_id")
|
||||
h.logger.Debug("userid:", userID)
|
||||
if userID != nil { // find userinfo from auth
|
||||
userIDValue := userID.(uint)
|
||||
req.Info.UserInfo.AuthUserID = userIDValue
|
||||
}
|
||||
|
||||
eventCh, err := h.chatUsecase.Chat(ctx, &req)
|
||||
if err != nil {
|
||||
return h.sendErrMsg(c, err.Error())
|
||||
}
|
||||
|
||||
for event := range eventCh {
|
||||
if err := h.writeSSEEvent(c, event); err != nil {
|
||||
return err
|
||||
}
|
||||
if event.Type == "done" || event.Type == "error" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChatWidget chat widget
|
||||
//
|
||||
// @Summary ChatWidget
|
||||
// @Description ChatWidget
|
||||
// @Tags Widget
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param app_type query string true "app type"
|
||||
// @Param request body domain.ChatRequest true "request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /share/v1/chat/widget [post]
|
||||
func (h *ShareChatHandler) ChatWidget(c echo.Context) error {
|
||||
var req domain.ChatRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Error("parse request failed", log.Error(err))
|
||||
return h.sendErrMsg(c, "parse request failed")
|
||||
}
|
||||
req.KBID = c.Request().Header.Get("X-KB-ID") // get from caddy header
|
||||
if err := c.Validate(&req); err != nil {
|
||||
h.logger.Error("validate request failed", log.Error(err))
|
||||
return h.sendErrMsg(c, "validate request failed")
|
||||
}
|
||||
if req.AppType != domain.AppTypeWidget {
|
||||
return h.sendErrMsg(c, "invalid app type")
|
||||
}
|
||||
if req.Message == "" && len(req.ImagePaths) == 0 {
|
||||
return h.sendErrMsg(c, "message is empty")
|
||||
}
|
||||
for _, path := range req.ImagePaths {
|
||||
if !strings.HasPrefix(path, "/static-file/") {
|
||||
return h.sendErrMsg(c, "invalid image path")
|
||||
}
|
||||
}
|
||||
|
||||
// get widget app info
|
||||
widgetAppInfo, err := h.appUsecase.GetWidgetAppInfo(c.Request().Context(), req.KBID)
|
||||
if err != nil {
|
||||
h.logger.Error("get widget app info failed", log.Error(err))
|
||||
return h.sendErrMsg(c, "get app info error")
|
||||
}
|
||||
if !widgetAppInfo.Settings.WidgetBotSettings.IsOpen {
|
||||
return h.sendErrMsg(c, "widget is not open")
|
||||
}
|
||||
|
||||
req.RemoteIP = c.RealIP()
|
||||
|
||||
c.Response().Header().Set("Content-Type", "text/event-stream")
|
||||
c.Response().Header().Set("Cache-Control", "no-cache")
|
||||
c.Response().Header().Set("Connection", "keep-alive")
|
||||
c.Response().Header().Set("Transfer-Encoding", "chunked")
|
||||
|
||||
eventCh, err := h.chatUsecase.Chat(c.Request().Context(), &req)
|
||||
if err != nil {
|
||||
return h.sendErrMsg(c, err.Error())
|
||||
}
|
||||
|
||||
for event := range eventCh {
|
||||
if err := h.writeSSEEvent(c, event); err != nil {
|
||||
return err
|
||||
}
|
||||
if event.Type == "done" || event.Type == "error" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ShareChatHandler) sendErrMsg(c echo.Context, errMsg string) error {
|
||||
return h.writeSSEEvent(c, domain.SSEEvent{Type: "error", Content: errMsg})
|
||||
}
|
||||
|
||||
func (h *ShareChatHandler) writeSSEEvent(c echo.Context, data any) error {
|
||||
jsonContent, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sseMessage := fmt.Sprintf("data: %s\n\n", string(jsonContent))
|
||||
if _, err := c.Response().Write([]byte(sseMessage)); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Response().Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
// FeedBack handle chat feedback
|
||||
//
|
||||
// @Summary Handle chat feedback
|
||||
// @Description Process user feedback for chat conversations
|
||||
// @Tags share_chat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.FeedbackRequest true "feedback request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /share/v1/chat/feedback [post]
|
||||
func (h *ShareChatHandler) FeedBack(c echo.Context) error {
|
||||
// 前端传入对应的conversationId和feedback内容,后端处理并返回反馈结果
|
||||
var feedbackReq domain.FeedbackRequest
|
||||
if err := c.Bind(&feedbackReq); err != nil {
|
||||
return h.NewResponseWithError(c, "bind feedback request failed", err)
|
||||
}
|
||||
if err := c.Validate(&feedbackReq); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
h.logger.Debug("receive feedback request:", log.Any("feedback_request", feedbackReq))
|
||||
if err := h.conversationUsecase.FeedBack(c.Request().Context(), &feedbackReq); err != nil {
|
||||
return h.NewResponseWithError(c, "handle feedback failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, "success")
|
||||
}
|
||||
|
||||
// ChatCompletions OpenAI API compatible chat completions
|
||||
//
|
||||
// @Summary ChatCompletions
|
||||
// @Description OpenAI API compatible chat completions endpoint
|
||||
// @Tags share_chat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "Knowledge Base ID"
|
||||
// @Param request body domain.OpenAICompletionsRequest true "OpenAI API request"
|
||||
// @Success 200 {object} domain.OpenAICompletionsResponse
|
||||
// @Failure 400 {object} domain.OpenAIErrorResponse
|
||||
// @Router /share/v1/chat/completions [post]
|
||||
func (h *ShareChatHandler) ChatCompletions(c echo.Context) error {
|
||||
var req domain.OpenAICompletionsRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Error("parse OpenAI request failed", log.Error(err))
|
||||
return h.sendOpenAIError(c, "parse request failed", "invalid_request_error")
|
||||
}
|
||||
|
||||
// get kb id from header
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.sendOpenAIError(c, "X-KB-ID header is required", "invalid_request_error")
|
||||
}
|
||||
|
||||
if err := c.Validate(&req); err != nil {
|
||||
h.logger.Error("validate OpenAI request failed", log.Error(err))
|
||||
return h.sendOpenAIError(c, "validate request failed", "invalid_request_error")
|
||||
}
|
||||
|
||||
// validate messages
|
||||
if len(req.Messages) == 0 {
|
||||
return h.sendOpenAIError(c, "messages cannot be empty", "invalid_request_error")
|
||||
}
|
||||
|
||||
// use last user message as message
|
||||
var lastUserMessage string
|
||||
for i := len(req.Messages) - 1; i >= 0; i-- {
|
||||
if req.Messages[i].Role == "user" {
|
||||
if req.Messages[i].Content != nil {
|
||||
lastUserMessage = req.Messages[i].Content.String()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if lastUserMessage == "" {
|
||||
return h.sendOpenAIError(c, "no user message found", "invalid_request_error")
|
||||
}
|
||||
|
||||
// validate api bot settings
|
||||
appBot, err := h.appUsecase.GetOpenAIAPIAppInfo(c.Request().Context(), kbID)
|
||||
if err != nil {
|
||||
return h.sendOpenAIError(c, err.Error(), "internal_error")
|
||||
}
|
||||
if !appBot.Settings.OpenAIAPIBotSettings.IsEnabled {
|
||||
return h.sendOpenAIError(c, "API Bot is not enabled", "forbidden")
|
||||
}
|
||||
|
||||
secretKeyHeader := c.Request().Header.Get("Authorization")
|
||||
if secretKeyHeader == "" {
|
||||
return h.sendOpenAIError(c, "Authorization header is required", "invalid_request_error")
|
||||
}
|
||||
if secretKey, found := strings.CutPrefix(secretKeyHeader, "Bearer "); !found {
|
||||
return h.sendOpenAIError(c, "Invalid Authorization key format", "invalid_request_error")
|
||||
} else {
|
||||
if appBot.Settings.OpenAIAPIBotSettings.SecretKey != secretKey {
|
||||
return h.sendOpenAIError(c, "Invalid Authorization key", "unauthorized")
|
||||
}
|
||||
}
|
||||
|
||||
chatReq := &domain.ChatRequest{
|
||||
Message: lastUserMessage,
|
||||
KBID: kbID,
|
||||
AppType: domain.AppTypeOpenAIAPI,
|
||||
RemoteIP: c.RealIP(),
|
||||
}
|
||||
|
||||
// set stream response header
|
||||
if req.Stream {
|
||||
c.Response().Header().Set("Content-Type", "text/event-stream")
|
||||
c.Response().Header().Set("Cache-Control", "no-cache")
|
||||
c.Response().Header().Set("Connection", "keep-alive")
|
||||
c.Response().Header().Set("Transfer-Encoding", "chunked")
|
||||
}
|
||||
|
||||
eventCh, err := h.chatUsecase.Chat(c.Request().Context(), chatReq)
|
||||
if err != nil {
|
||||
return h.sendOpenAIError(c, err.Error(), "internal_error")
|
||||
}
|
||||
|
||||
// handle stream response
|
||||
if req.Stream {
|
||||
return h.handleOpenAIStreamResponse(c, eventCh, req.Model)
|
||||
} else {
|
||||
return h.handleOpenAINonStreamResponse(c, eventCh, req.Model)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ShareChatHandler) handleOpenAIStreamResponse(c echo.Context, eventCh <-chan domain.SSEEvent, model string) error {
|
||||
responseID := "chatcmpl-" + generateID()
|
||||
created := time.Now().Unix()
|
||||
|
||||
for event := range eventCh {
|
||||
switch event.Type {
|
||||
case "error":
|
||||
return h.sendOpenAIError(c, event.Content, "internal_error")
|
||||
case "data":
|
||||
// send stream response
|
||||
streamResp := domain.OpenAIStreamResponse{
|
||||
ID: responseID,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: created,
|
||||
Model: model,
|
||||
Choices: []domain.OpenAIStreamChoice{
|
||||
{
|
||||
Index: 0,
|
||||
Delta: domain.OpenAIMessage{
|
||||
Role: "assistant",
|
||||
Content: domain.NewStringContent(event.Content),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := h.writeOpenAIStreamEvent(c, streamResp); err != nil {
|
||||
return err
|
||||
}
|
||||
case "done":
|
||||
// send done event
|
||||
streamResp := domain.OpenAIStreamResponse{
|
||||
ID: responseID,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: created,
|
||||
Model: model,
|
||||
Choices: []domain.OpenAIStreamChoice{
|
||||
{
|
||||
Index: 0,
|
||||
Delta: domain.OpenAIMessage{},
|
||||
FinishReason: stringPtr("stop"),
|
||||
},
|
||||
},
|
||||
}
|
||||
return h.writeOpenAIStreamEvent(c, streamResp)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ShareChatHandler) handleOpenAINonStreamResponse(c echo.Context, eventCh <-chan domain.SSEEvent, model string) error {
|
||||
responseID := "chatcmpl-" + generateID()
|
||||
created := time.Now().Unix()
|
||||
|
||||
var content string
|
||||
for event := range eventCh {
|
||||
switch event.Type {
|
||||
case "error":
|
||||
return h.sendOpenAIError(c, event.Content, "internal_error")
|
||||
case "data":
|
||||
content += event.Content
|
||||
case "done":
|
||||
// send complete response
|
||||
resp := domain.OpenAICompletionsResponse{
|
||||
ID: responseID,
|
||||
Object: "chat.completion",
|
||||
Created: created,
|
||||
Model: model,
|
||||
Choices: []domain.OpenAIChoice{
|
||||
{
|
||||
Index: 0,
|
||||
Message: domain.OpenAIMessage{
|
||||
Role: "assistant",
|
||||
Content: domain.NewStringContent(content),
|
||||
},
|
||||
FinishReason: "stop",
|
||||
},
|
||||
},
|
||||
}
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ShareChatHandler) sendOpenAIError(c echo.Context, message, errorType string) error {
|
||||
errResp := domain.OpenAIErrorResponse{
|
||||
Error: domain.OpenAIError{
|
||||
Message: message,
|
||||
Type: errorType,
|
||||
},
|
||||
}
|
||||
return c.JSON(http.StatusBadRequest, errResp)
|
||||
}
|
||||
|
||||
func (h *ShareChatHandler) writeOpenAIStreamEvent(c echo.Context, data domain.OpenAIStreamResponse) error {
|
||||
jsonContent, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sseMessage := fmt.Sprintf("data: %s\n\n", string(jsonContent))
|
||||
if _, err := c.Response().Write([]byte(sseMessage)); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Response().Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateID() string {
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
// ChatSearch searches chat messages in shared knowledge base
|
||||
//
|
||||
// @Summary ChatSearch
|
||||
// @Description ChatSearch
|
||||
// @Tags share_chat_search
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.ChatSearchReq true "request"
|
||||
// @Success 200 {object} domain.Response{data=domain.ChatSearchResp}
|
||||
// @Router /share/v1/chat/search [post]
|
||||
func (h *ShareChatHandler) ChatSearch(c echo.Context) error {
|
||||
var req domain.ChatSearchReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "parse request failed", err)
|
||||
}
|
||||
req.KBID = c.Request().Header.Get("X-KB-ID") // get from caddy header
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
// validate captcha token
|
||||
if !h.Captcha.ValidateToken(ctx, req.CaptchaToken) {
|
||||
return h.NewResponseWithError(c, "invalid captcha token", nil)
|
||||
}
|
||||
|
||||
req.RemoteIP = c.RealIP()
|
||||
|
||||
// get user info --> no enterprise is nil
|
||||
userID := c.Get("user_id")
|
||||
if userID != nil {
|
||||
if userIDValue, ok := userID.(uint); ok {
|
||||
req.AuthUserID = userIDValue
|
||||
} else {
|
||||
return h.NewResponseWithError(c, "invalid user id type", nil)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := h.chatUsecase.Search(ctx, &req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to search docs", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
||||
// WidgetSearch
|
||||
//
|
||||
// @Summary WidgetSearch
|
||||
// @Description WidgetSearch
|
||||
// @Tags Widget
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.ChatSearchReq true "Comment"
|
||||
// @Success 200 {object} domain.Response{data=domain.ChatSearchResp}
|
||||
// @Router /share/v1/chat/widget/search [post]
|
||||
func (h *ShareChatHandler) WidgetSearch(c echo.Context) error {
|
||||
var req domain.ChatSearchReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "parse request failed", err)
|
||||
}
|
||||
req.KBID = c.Request().Header.Get("X-KB-ID")
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// validate widget info
|
||||
widgetAppInfo, err := h.appUsecase.GetWidgetAppInfo(c.Request().Context(), req.KBID)
|
||||
if err != nil {
|
||||
h.logger.Error("get widget app info failed", log.Error(err))
|
||||
return h.sendErrMsg(c, "get app info error")
|
||||
}
|
||||
if !widgetAppInfo.Settings.WidgetBotSettings.IsOpen {
|
||||
return h.sendErrMsg(c, "widget is not open")
|
||||
}
|
||||
|
||||
req.RemoteIP = c.RealIP()
|
||||
|
||||
resp, err := h.chatUsecase.Search(ctx, &req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to search docs", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
165
backend/handler/share/comment.go
Normal file
165
backend/handler/share/comment.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type ShareCommentHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
usecase *usecase.CommentUsecase
|
||||
app *usecase.AppUsecase
|
||||
}
|
||||
|
||||
func NewShareCommentHandler(
|
||||
e *echo.Echo,
|
||||
baseHandler *handler.BaseHandler,
|
||||
logger *log.Logger,
|
||||
usecase *usecase.CommentUsecase,
|
||||
app *usecase.AppUsecase,
|
||||
) *ShareCommentHandler {
|
||||
h := &ShareCommentHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.share.comment"),
|
||||
usecase: usecase,
|
||||
app: app,
|
||||
}
|
||||
|
||||
share := e.Group("share/v1/comment",
|
||||
func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Response().Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
c.Response().Header().Set("Access-Control-Allow-Headers", "Content-Type, Origin, Accept")
|
||||
if c.Request().Method == "OPTIONS" {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}, h.ShareAuthMiddleware.Authorize)
|
||||
|
||||
share.POST("", h.CreateComment)
|
||||
share.GET("/list", h.GetCommentList)
|
||||
return h
|
||||
}
|
||||
|
||||
// CreateComment
|
||||
//
|
||||
// @Summary CreateComment
|
||||
// @Description CreateComment
|
||||
// @Tags share_comment
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param comment body domain.CommentReq true "Comment"
|
||||
// @Success 200 {object} domain.PWResponse{data=string} "CommentID"
|
||||
// @Router /share/v1/comment [post]
|
||||
func (h *ShareCommentHandler) CreateComment(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
var req domain.CommentReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "bind comment request failed", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate req failed", err)
|
||||
}
|
||||
// 校验是否开启了评论
|
||||
appInfo, err := h.app.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppType(domain.AppTypeWeb))
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "app info is not found", err)
|
||||
}
|
||||
if !appInfo.Settings.WebAppCommentSettings.IsEnable {
|
||||
return h.NewResponseWithError(c, "please check comment is open", nil)
|
||||
}
|
||||
// validate captcha token
|
||||
if !h.Captcha.ValidateToken(ctx, req.CaptchaToken) {
|
||||
return h.NewResponseWithError(c, "failed to validate captcha token", nil)
|
||||
}
|
||||
|
||||
for _, url := range req.PicUrls {
|
||||
if !strings.HasPrefix(url, "/static-file/") {
|
||||
return h.NewResponseWithError(c, "validate param pic_urls failed", err)
|
||||
}
|
||||
}
|
||||
|
||||
remoteIP := c.RealIP()
|
||||
|
||||
// get user info --> no enterprise is nil
|
||||
var userIDValue uint
|
||||
userID := c.Get("user_id")
|
||||
if userID != nil { // can find userinfo from auth
|
||||
userIDValue = userID.(uint)
|
||||
}
|
||||
|
||||
var status = 1 // no moderate
|
||||
// 判断user is moderate comment ---> 默认false
|
||||
if appInfo.Settings.WebAppCommentSettings.ModerationEnable {
|
||||
status = 0
|
||||
}
|
||||
commentStatus := domain.CommentStatus(status)
|
||||
|
||||
// 插入到数据库中
|
||||
commentID, err := h.usecase.CreateComment(ctx, &req, kbID, remoteIP, commentStatus, userIDValue)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "create comment failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, commentID)
|
||||
}
|
||||
|
||||
type ShareCommentLists = *domain.PaginatedResult[[]*domain.ShareCommentListItem]
|
||||
|
||||
// GetCommentList
|
||||
//
|
||||
// @Summary GetCommentList
|
||||
// @Description GetCommentList
|
||||
// @Tags share_comment
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query string true "nodeID"
|
||||
// @Success 200 {object} domain.PWResponse{data=ShareCommentLists} "CommentList
|
||||
// @Router /share/v1/comment/list [get]
|
||||
func (h *ShareCommentHandler) GetCommentList(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
// 拿到node_id即可
|
||||
nodeID := c.QueryParam("id")
|
||||
if nodeID == "" {
|
||||
return h.NewResponseWithError(c, "node id is required", nil)
|
||||
}
|
||||
|
||||
// 校验是否开启了评论
|
||||
appInfo, err := h.app.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppType(domain.AppTypeWeb))
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "app info is not found", err)
|
||||
}
|
||||
if !appInfo.Settings.WebAppCommentSettings.IsEnable {
|
||||
return h.NewResponseWithError(c, "please check comment is open", nil)
|
||||
}
|
||||
|
||||
// 查询数据库获取所有评论-->0 所有, 1,2 为需要审核的评论
|
||||
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get comment list", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, commentsList)
|
||||
}
|
||||
157
backend/handler/share/common.go
Normal file
157
backend/handler/share/common.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/share/v1"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
"github.com/chaitin/panda-wiki/utils"
|
||||
)
|
||||
|
||||
type ShareCommonHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
fileUsecase *usecase.FileUsecase
|
||||
}
|
||||
|
||||
func NewShareCommonHandler(
|
||||
e *echo.Echo,
|
||||
baseHandler *handler.BaseHandler,
|
||||
logger *log.Logger,
|
||||
fileUsecase *usecase.FileUsecase,
|
||||
) *ShareCommonHandler {
|
||||
h := &ShareCommonHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger,
|
||||
fileUsecase: fileUsecase,
|
||||
}
|
||||
|
||||
share := e.Group("share/v1/common",
|
||||
func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Response().Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
c.Response().Header().Set("Access-Control-Allow-Headers", "Content-Type, Origin, Accept")
|
||||
if c.Request().Method == "OPTIONS" {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
share.POST("/file/upload", h.FileUpload, h.ShareAuthMiddleware.Authorize)
|
||||
share.POST("/file/upload/url", h.FileUploadByUrl, h.ShareAuthMiddleware.Authorize)
|
||||
return h
|
||||
}
|
||||
|
||||
// FileUpload 文件上传
|
||||
//
|
||||
// @Tags ShareFile
|
||||
// @Summary 文件上传
|
||||
// @Description 前台用户上传文件,目前只支持图片文件上传
|
||||
// @ID share-FileUpload
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb id"
|
||||
// @Param file formData file true "File"
|
||||
// @Param captcha_token formData string true "captcha_token"
|
||||
// @Success 200 {object} domain.Response{data=v1.FileUploadResp}
|
||||
// @Router /share/v1/common/file/upload [post]
|
||||
func (h *ShareCommonHandler) FileUpload(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var req v1.ShareFileUploadReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request parameters", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
req.KbId = kbID
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get file", err)
|
||||
}
|
||||
|
||||
if !utils.IsImageFile(file.Filename) {
|
||||
return h.NewResponseWithError(c, "只支持图片文件上传", fmt.Errorf("unsupported file type: %s", file.Filename))
|
||||
}
|
||||
|
||||
// validate captcha token
|
||||
if !h.Captcha.ValidateToken(ctx, req.CaptchaToken) {
|
||||
return h.NewResponseWithError(c, "failed to validate captcha token", nil)
|
||||
}
|
||||
|
||||
key, err := h.fileUsecase.UploadFile(ctx, req.KbId, file)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "upload failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, v1.FileUploadResp{
|
||||
Key: key,
|
||||
})
|
||||
}
|
||||
|
||||
// FileUploadByUrl 通过url上传文件
|
||||
//
|
||||
// @Tags ShareFile
|
||||
// @Summary 文件上传
|
||||
// @Description 前台用户上传文件,目前只支持图片文件上传
|
||||
// @ID share-FileUploadByUrl
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body v1.ShareFileUploadUrlReq true "body"
|
||||
// @Success 200 {object} domain.Response{data=v1.ShareFileUploadUrlResp}
|
||||
// @Router /share/v1/common/file/upload/url [post]
|
||||
func (h *ShareCommonHandler) FileUploadByUrl(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var req v1.ShareFileUploadUrlReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request parameters", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
req.KbId = kbID
|
||||
|
||||
parsedURL, err := url.Parse(req.Url)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "invalid URL format", err)
|
||||
}
|
||||
if !utils.IsImageFile(parsedURL.Path) {
|
||||
return h.NewResponseWithError(c, "只支持图片文件上传", fmt.Errorf("unsupported file type: %s", req.Url))
|
||||
}
|
||||
|
||||
// validate captcha token
|
||||
if !h.Captcha.ValidateToken(ctx, req.CaptchaToken) {
|
||||
return h.NewResponseWithError(c, "failed to validate captcha token", nil)
|
||||
}
|
||||
|
||||
key, err := h.fileUsecase.UploadFileByUrl(ctx, req.KbId, req.Url)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "upload failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, v1.ShareFileUploadUrlResp{
|
||||
Key: key,
|
||||
})
|
||||
}
|
||||
63
backend/handler/share/coversation.go
Normal file
63
backend/handler/share/coversation.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type ShareConversationHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
usecase *usecase.ConversationUsecase
|
||||
}
|
||||
|
||||
func NewShareConversationHandler(
|
||||
baseHandler *handler.BaseHandler,
|
||||
echo *echo.Echo,
|
||||
usecase *usecase.ConversationUsecase,
|
||||
logger *log.Logger,
|
||||
) *ShareConversationHandler {
|
||||
h := &ShareConversationHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.share.conversation"),
|
||||
usecase: usecase,
|
||||
}
|
||||
|
||||
group := echo.Group("share/v1/conversation",
|
||||
h.ShareAuthMiddleware.Authorize,
|
||||
)
|
||||
group.GET("/detail", h.GetConversationDetail)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// GetConversationDetail
|
||||
//
|
||||
// @Summary GetConversationDetail
|
||||
// @Description GetConversationDetail
|
||||
// @Tags share_conversation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb id"
|
||||
// @Param id query string true "conversation id"
|
||||
// @Success 200 {object} domain.PWResponse{data=domain.ShareConversationDetailResp}
|
||||
// @Router /share/v1/conversation/detail [get]
|
||||
func (h *ShareConversationHandler) GetConversationDetail(c echo.Context) error {
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
id := c.QueryParam("id")
|
||||
if id == "" {
|
||||
return h.NewResponseWithError(c, "id is required", nil)
|
||||
}
|
||||
|
||||
node, err := h.usecase.GetShareConversationDetail(c.Request().Context(), kbID, id)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get node detail", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, node)
|
||||
}
|
||||
67
backend/handler/share/nav.go
Normal file
67
backend/handler/share/nav.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/share/v1"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type ShareNavHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
usecase *usecase.NavUsecase
|
||||
}
|
||||
|
||||
func NewShareNavHandler(
|
||||
baseHandler *handler.BaseHandler,
|
||||
echo *echo.Echo,
|
||||
usecase *usecase.NavUsecase,
|
||||
logger *log.Logger,
|
||||
) *ShareNavHandler {
|
||||
h := &ShareNavHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.share.nav"),
|
||||
usecase: usecase,
|
||||
}
|
||||
|
||||
group := echo.Group("share/v1/nav",
|
||||
h.ShareAuthMiddleware.Authorize,
|
||||
)
|
||||
group.GET("/list", h.ShareNavList)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// ShareNavList
|
||||
//
|
||||
// @Summary 前台获取栏目列表
|
||||
// @Description ShareNavList
|
||||
// @Tags share_nav
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param query v1.ShareNavListReq true "para"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /share/v1/nav/list [get]
|
||||
func (h *ShareNavHandler) ShareNavList(c echo.Context) error {
|
||||
|
||||
var req v1.ShareNavListReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
h.logger.Error("parse request failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "parse request failed", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(&req); err != nil {
|
||||
h.logger.Error("validate request failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
navs, err := h.usecase.GetReleaseList(c.Request().Context(), req.KbId)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get nav list", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, navs)
|
||||
}
|
||||
106
backend/handler/share/node.go
Normal file
106
backend/handler/share/node.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type ShareNodeHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
usecase *usecase.NodeUsecase
|
||||
}
|
||||
|
||||
func NewShareNodeHandler(
|
||||
baseHandler *handler.BaseHandler,
|
||||
echo *echo.Echo,
|
||||
usecase *usecase.NodeUsecase,
|
||||
logger *log.Logger,
|
||||
) *ShareNodeHandler {
|
||||
h := &ShareNodeHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.share.node"),
|
||||
usecase: usecase,
|
||||
}
|
||||
|
||||
group := echo.Group("share/v1/node",
|
||||
h.ShareAuthMiddleware.Authorize,
|
||||
)
|
||||
group.GET("/list", h.ShareNodeList)
|
||||
group.GET("/detail", h.GetNodeDetail)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// ShareNodeList
|
||||
//
|
||||
// @Summary ShareNodeList
|
||||
// @Description ShareNodeList
|
||||
// @Tags share_node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb id"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /share/v1/node/list [get]
|
||||
func (h *ShareNodeHandler) ShareNodeList(c echo.Context) error {
|
||||
|
||||
kbId := c.Request().Header.Get("X-KB-ID")
|
||||
if kbId == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
nodes, err := h.usecase.GetShareNodeList(c.Request().Context(), kbId, domain.GetAuthID(c))
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get node list", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nodes)
|
||||
}
|
||||
|
||||
// GetNodeDetail
|
||||
//
|
||||
// @Summary GetNodeDetail
|
||||
// @Description GetNodeDetail
|
||||
// @Tags share_node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-KB-ID header string true "kb id"
|
||||
// @Param id query string true "node id"
|
||||
// @Param format query string true "format"
|
||||
// @Success 200 {object} domain.Response{data=v1.ShareNodeDetailResp}
|
||||
// @Router /share/v1/node/detail [get]
|
||||
func (h *ShareNodeHandler) GetNodeDetail(c echo.Context) error {
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
id := c.QueryParam("id")
|
||||
if id == "" {
|
||||
return h.NewResponseWithError(c, "id is required", nil)
|
||||
}
|
||||
|
||||
errCode := h.usecase.ValidateNodePerm(c.Request().Context(), kbID, id, domain.GetAuthID(c))
|
||||
if errCode != nil {
|
||||
return h.NewResponseWithErrCode(c, *errCode)
|
||||
}
|
||||
|
||||
node, err := h.usecase.GetNodeReleaseDetailByKBIDAndID(c.Request().Context(), kbID, id, c.QueryParam("format"))
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get node detail", err)
|
||||
}
|
||||
|
||||
// If the node is a folder, return the list of child nodes
|
||||
if node.Type == domain.NodeTypeFolder {
|
||||
childNodes, err := h.usecase.GetNodeReleaseListByParentID(c.Request().Context(), kbID, id, domain.GetAuthID(c))
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get child nodes", err)
|
||||
}
|
||||
node.List = childNodes
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, node)
|
||||
}
|
||||
157
backend/handler/share/openapi.go
Normal file
157
backend/handler/share/openapi.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
larkevent "github.com/larksuite/oapi-sdk-go/v3/event"
|
||||
"github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/share/v1"
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type OpenapiV1Handler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
authUseCase *usecase.AuthUsecase
|
||||
appCase *usecase.AppUsecase
|
||||
}
|
||||
|
||||
func NewOpenapiV1Handler(
|
||||
e *echo.Echo,
|
||||
baseHandler *handler.BaseHandler,
|
||||
logger *log.Logger,
|
||||
authUseCase *usecase.AuthUsecase,
|
||||
appCase *usecase.AppUsecase,
|
||||
) *OpenapiV1Handler {
|
||||
h := &OpenapiV1Handler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger,
|
||||
authUseCase: authUseCase,
|
||||
appCase: appCase,
|
||||
}
|
||||
|
||||
OpenapiGroup := e.Group("/share/v1/openapi")
|
||||
|
||||
OpenapiGroup.Any("/github/callback", h.GitHubCallback)
|
||||
|
||||
// lark机器人
|
||||
OpenapiGroup.POST("/lark/bot/:kb_id", h.LarkBot)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// GitHubCallback GitHub回调
|
||||
//
|
||||
// @Tags ShareOpenapi
|
||||
// @Summary GitHub回调
|
||||
// @Description GitHub回调
|
||||
// @ID v1-GitHubCallback
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param query v1.GitHubCallbackReq true "para"
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.GitHubCallbackResp}
|
||||
// @Router /share/v1/openapi/github/callback [get]
|
||||
func (h *OpenapiV1Handler) GitHubCallback(c echo.Context) error {
|
||||
ctx := context.WithValue(c.Request().Context(), consts.ContextKeyEdition, consts.GetLicenseEdition(c))
|
||||
|
||||
var req v1.GitHubCallbackReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Code == "" {
|
||||
return h.NewResponseWithError(c, "code is required", nil)
|
||||
}
|
||||
|
||||
auth, redirectUrl, err := h.authUseCase.GitHubCallback(ctx, req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "handle callback failed", err)
|
||||
}
|
||||
|
||||
if err := h.authUseCase.SaveNewSession(c, auth); err != nil {
|
||||
return h.NewResponseWithError(c, "save session failed", err)
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusFound, redirectUrl)
|
||||
}
|
||||
|
||||
// LarkBot Lark机器人请求
|
||||
//
|
||||
// @Tags ShareOpenapi
|
||||
// @Summary Lark机器人请求
|
||||
// @Description Lark机器人请求
|
||||
// @ID v1-LarkBot
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param kb_id path string true "知识库ID"
|
||||
// @Success 200 {object} domain.PWResponse
|
||||
// @Router /share/v1/openapi/lark/bot/{kb_id} [post]
|
||||
func (h *OpenapiV1Handler) LarkBot(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
kbID := c.Param("kb_id")
|
||||
if kbID == "" {
|
||||
h.logger.Error("kb_id is required")
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
// 获取应用配置
|
||||
appInfo, err := h.appCase.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppTypeLarkBot)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get app detail", log.Error(err), log.String("kb_id", kbID))
|
||||
return h.NewResponseWithError(c, "failed to get app detail", err)
|
||||
}
|
||||
|
||||
if appInfo.Settings.LarkBotSettings.IsEnabled == nil || !*appInfo.Settings.LarkBotSettings.IsEnabled {
|
||||
h.logger.Error("lark bot is not enabled")
|
||||
return h.NewResponseWithError(c, "lark bot is not enabled", err)
|
||||
}
|
||||
|
||||
var eventHandler *dispatcher.EventDispatcher
|
||||
client, ok := h.appCase.GetLarkBotClient(appInfo.ID)
|
||||
if ok {
|
||||
eventHandler = client.GetEventHandler()
|
||||
}
|
||||
|
||||
if eventHandler == nil {
|
||||
eventHandler = dispatcher.NewEventDispatcher(
|
||||
appInfo.Settings.LarkBotSettings.VerifyToken,
|
||||
appInfo.Settings.LarkBotSettings.EncryptKey,
|
||||
)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to read request body", log.Error(err))
|
||||
return h.NewResponseWithError(c, "failed to read request body", err)
|
||||
}
|
||||
defer c.Request().Body.Close()
|
||||
|
||||
eventReq := &larkevent.EventReq{
|
||||
Header: c.Request().Header,
|
||||
Body: body,
|
||||
RequestURI: c.Request().RequestURI,
|
||||
}
|
||||
|
||||
eventResp := eventHandler.Handle(ctx, eventReq)
|
||||
if eventResp == nil {
|
||||
h.logger.Error("failed to handle lark event: nil response")
|
||||
return h.NewResponseWithError(c, "failed to handle lark event", errors.New("nil response"))
|
||||
}
|
||||
|
||||
for key, values := range eventResp.Header {
|
||||
for _, value := range values {
|
||||
c.Response().Header().Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSONBlob(eventResp.StatusCode, eventResp.Body)
|
||||
}
|
||||
43
backend/handler/share/provider.go
Normal file
43
backend/handler/share/provider.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/chaitin/panda-wiki/pkg/captcha"
|
||||
)
|
||||
|
||||
type ShareHandler struct {
|
||||
ShareNodeHandler *ShareNodeHandler
|
||||
ShareNavHandler *ShareNavHandler
|
||||
ShareAppHandler *ShareAppHandler
|
||||
ShareChatHandler *ShareChatHandler
|
||||
ShareSitemapHandler *ShareSitemapHandler
|
||||
ShareStatHandler *ShareStatHandler
|
||||
ShareCommentHandler *ShareCommentHandler
|
||||
ShareAuthHandler *ShareAuthHandler
|
||||
ShareConversationHandler *ShareConversationHandler
|
||||
ShareWechatHandler *ShareWechatHandler
|
||||
ShareCaptchaHandler *ShareCaptchaHandler
|
||||
OpenapiV1Handler *OpenapiV1Handler
|
||||
ShareCommonHandler *ShareCommonHandler
|
||||
}
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
captcha.NewCaptcha,
|
||||
|
||||
NewShareNodeHandler,
|
||||
NewShareNavHandler,
|
||||
NewShareAppHandler,
|
||||
NewShareChatHandler,
|
||||
NewShareSitemapHandler,
|
||||
NewShareStatHandler,
|
||||
NewShareCommentHandler,
|
||||
NewShareAuthHandler,
|
||||
NewShareConversationHandler,
|
||||
NewShareWechatHandler,
|
||||
NewShareCaptchaHandler,
|
||||
NewShareCommonHandler,
|
||||
NewOpenapiV1Handler,
|
||||
|
||||
wire.Struct(new(ShareHandler), "*"),
|
||||
)
|
||||
46
backend/handler/share/sitemap.go
Normal file
46
backend/handler/share/sitemap.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type ShareSitemapHandler struct {
|
||||
*handler.BaseHandler
|
||||
sitemapUsecase *usecase.SitemapUsecase
|
||||
appUsecase *usecase.AppUsecase
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewShareSitemapHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, sitemapUsecase *usecase.SitemapUsecase, appUsecase *usecase.AppUsecase, logger *log.Logger) *ShareSitemapHandler {
|
||||
h := &ShareSitemapHandler{
|
||||
BaseHandler: baseHandler,
|
||||
sitemapUsecase: sitemapUsecase,
|
||||
appUsecase: appUsecase,
|
||||
logger: logger.WithModule("handler.share.sitemap"),
|
||||
}
|
||||
|
||||
group := echo.Group("/sitemap.xml")
|
||||
group.GET("", h.GetSitemap)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *ShareSitemapHandler) GetSitemap(c echo.Context) error {
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
xml, err := h.sitemapUsecase.GetSitemap(c.Request().Context(), kbID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to generate sitemap", err)
|
||||
}
|
||||
|
||||
return c.Blob(http.StatusOK, echo.MIMEApplicationXMLCharsetUTF8, []byte(xml))
|
||||
}
|
||||
102
backend/handler/share/stat.go
Normal file
102
backend/handler/share/stat.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mileusna/useragent"
|
||||
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type ShareStatHandler struct {
|
||||
*handler.BaseHandler
|
||||
useCase *usecase.StatUseCase
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewShareStatHandler(baseHandler *handler.BaseHandler, echo *echo.Echo, useCase *usecase.StatUseCase, logger *log.Logger) *ShareStatHandler {
|
||||
h := &ShareStatHandler{
|
||||
BaseHandler: baseHandler,
|
||||
useCase: useCase,
|
||||
logger: logger.WithModule("handler.share.stat"),
|
||||
}
|
||||
|
||||
group := echo.Group("/share/v1/stat")
|
||||
group.POST("/page", h.RecordPage, h.ShareAuthMiddleware.Authorize)
|
||||
return h
|
||||
}
|
||||
|
||||
// RecordPage record page
|
||||
//
|
||||
// @Summary RecordPage
|
||||
// @Description RecordPage
|
||||
// @Tags share_stat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.StatPageReq true "request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /share/v1/stat/page [post]
|
||||
func (h *ShareStatHandler) RecordPage(c echo.Context) error {
|
||||
req := &domain.StatPageReq{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return h.NewResponseWithError(c, "bind request body failed", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
// get user info --> no enterprise is nil
|
||||
var userIDValue uint
|
||||
userID := c.Get("user_id")
|
||||
if userID != nil { // can find userinfo from auth
|
||||
userIDValue = userID.(uint)
|
||||
}
|
||||
|
||||
ua := c.Request().UserAgent()
|
||||
userAgent := useragent.Parse(ua)
|
||||
browserName := userAgent.Name
|
||||
browserOS := userAgent.OS
|
||||
referer := c.Request().Referer()
|
||||
refererHost := ""
|
||||
if referer != "" {
|
||||
refererURL, err := url.Parse(referer)
|
||||
if err == nil {
|
||||
refererHost = refererURL.Host
|
||||
}
|
||||
}
|
||||
sessionID := ""
|
||||
sessionIDCookie, err := c.Request().Cookie("x-pw-session-id")
|
||||
if err != nil {
|
||||
sessionID = c.Request().Header.Get("x-pw-session-id")
|
||||
} else {
|
||||
sessionID = sessionIDCookie.Value
|
||||
}
|
||||
if sessionID == "" {
|
||||
return h.NewResponseWithError(c, "session id not found", err)
|
||||
}
|
||||
ip := c.RealIP()
|
||||
stat := &domain.StatPage{
|
||||
KBID: kbID,
|
||||
UserID: userIDValue,
|
||||
NodeID: req.NodeID,
|
||||
Scene: req.Scene,
|
||||
SessionID: sessionID,
|
||||
IP: ip,
|
||||
UA: ua,
|
||||
BrowserName: browserName,
|
||||
BrowserOS: browserOS,
|
||||
Referer: referer,
|
||||
RefererHost: refererHost,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := h.useCase.RecordPage(c.Request().Context(), stat); err != nil {
|
||||
return h.NewResponseWithError(c, "record page failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
489
backend/handler/share/wechat.go
Normal file
489
backend/handler/share/wechat.go
Normal file
@@ -0,0 +1,489 @@
|
||||
package share
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/sbzhu/weworkapi_golang/wxbizmsgcrypt"
|
||||
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/pkg/bot/wechat"
|
||||
"github.com/chaitin/panda-wiki/pkg/bot/wechat_service"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type ShareWechatHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
appCase *usecase.AppUsecase
|
||||
conversationCase *usecase.ConversationUsecase
|
||||
wechatUsecase *usecase.WechatServiceUsecase
|
||||
wecomUsecase *usecase.WecomUsecase
|
||||
wechatAppUsecase *usecase.WechatAppUsecase
|
||||
}
|
||||
|
||||
func NewShareWechatHandler(
|
||||
e *echo.Echo,
|
||||
baseHandler *handler.BaseHandler,
|
||||
logger *log.Logger,
|
||||
appCase *usecase.AppUsecase,
|
||||
conversationCase *usecase.ConversationUsecase,
|
||||
wechatUsecase *usecase.WechatServiceUsecase,
|
||||
wecomUsecase *usecase.WecomUsecase,
|
||||
wechatAppUsecase *usecase.WechatAppUsecase,
|
||||
) *ShareWechatHandler {
|
||||
h := &ShareWechatHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.share.wechat"),
|
||||
appCase: appCase,
|
||||
conversationCase: conversationCase,
|
||||
wechatUsecase: wechatUsecase,
|
||||
wecomUsecase: wecomUsecase,
|
||||
wechatAppUsecase: wechatAppUsecase,
|
||||
}
|
||||
|
||||
share := e.Group("share/v1/app",
|
||||
func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
c.Response().Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Response().Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
c.Response().Header().Set("Access-Control-Allow-Headers", "Content-Type, Origin, Accept")
|
||||
if c.Request().Method == "OPTIONS" {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
// 微信客服
|
||||
share.GET("/wechat/service", h.VerifyUrlWechatService)
|
||||
share.POST("/wechat/service", h.WechatHandlerService)
|
||||
|
||||
share.GET("/wechat/service/answer", h.GetWechatAnswer)
|
||||
//企业微信
|
||||
share.GET("/wechat/app", h.VerifyUrlWechatApp)
|
||||
share.POST("/wechat/app", h.WechatHandlerApp)
|
||||
|
||||
// 企业微信智能机器人
|
||||
share.GET("/wecom/ai_bot", h.WecomAIBotVerify)
|
||||
share.POST("/wecom/ai_bot", h.WecomAIBotHandle)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// GetWechatAnswer
|
||||
//
|
||||
// @Summary GetWechatAnswer
|
||||
// @Description GetWechatAnswer
|
||||
// @Tags Wechat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query string true "conversation id"
|
||||
// @Success 200 {object} domain.Response
|
||||
//
|
||||
// @Router /share/v1/app/wechat/service/answer [get]
|
||||
func (h *ShareWechatHandler) GetWechatAnswer(c echo.Context) error {
|
||||
conversationID := c.QueryParam("id")
|
||||
if conversationID == "" {
|
||||
return h.NewResponseWithError(c, "conversation_id is required", nil)
|
||||
}
|
||||
|
||||
c.Response().Header().Set("Content-Type", "text/event-stream")
|
||||
c.Response().Header().Set("Cache-Control", "no-cache")
|
||||
c.Response().Header().Set("Connection", "keep-alive")
|
||||
c.Response().Header().Set("Transfer-Encoding", "chunked")
|
||||
|
||||
// checkout if the conversation exists in map
|
||||
val, ok := domain.ConversationManager.Load(conversationID)
|
||||
if !ok { // not exist check db
|
||||
conversation, err := h.conversationCase.GetConversationDetail(c.Request().Context(), "", conversationID)
|
||||
if err != nil {
|
||||
return h.sendErrMsg(c, err.Error())
|
||||
}
|
||||
// send answer and question
|
||||
if err := h.writeSSEEvent(c, domain.SSEEvent{Type: "question", Content: conversation.Messages[0].Content}); err != nil {
|
||||
return err
|
||||
}
|
||||
//2.answer
|
||||
if err := h.writeSSEEvent(c, domain.SSEEvent{Type: "feedback_score", Content: strconv.Itoa(int(conversation.Messages[1].Info.Score))}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.writeSSEEvent(c, domain.SSEEvent{Type: "message_id", Content: conversation.Messages[1].ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.writeSSEEvent(c, domain.SSEEvent{Type: "answer", Content: conversation.Messages[1].Content}); err != nil {
|
||||
return err
|
||||
}
|
||||
//3.
|
||||
if err := h.writeSSEEvent(c, domain.SSEEvent{Type: "done", Content: ""}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// exit --> get message
|
||||
state := val.(*domain.ConversationState)
|
||||
// 1. send question
|
||||
if err := h.writeSSEEvent(c, domain.SSEEvent{Type: "question", Content: state.Question}); err != nil {
|
||||
return err
|
||||
}
|
||||
//2. send answer
|
||||
state.Mutex.Lock()
|
||||
if err := h.writeSSEEvent(c, domain.SSEEvent{Type: "answer", Content: state.Buffer.String()}); err != nil {
|
||||
return err
|
||||
}
|
||||
state.IsVisited = true
|
||||
state.Mutex.Unlock()
|
||||
|
||||
defer func() {
|
||||
state.Mutex.Lock()
|
||||
state.IsVisited = false
|
||||
state.Mutex.Unlock()
|
||||
}()
|
||||
|
||||
for answer := range state.NotificationChan { // listen if has new data
|
||||
if err := h.writeSSEEvent(c, domain.SSEEvent{Type: "answer", Content: answer}); err != nil {
|
||||
return err
|
||||
} // catch err
|
||||
}
|
||||
|
||||
return h.writeSSEEvent(c, domain.SSEEvent{Type: "done", Content: ""})
|
||||
}
|
||||
|
||||
func (h *ShareWechatHandler) sendErrMsg(c echo.Context, errMsg string) error {
|
||||
return h.writeSSEEvent(c, domain.SSEEvent{Type: "error", Content: errMsg})
|
||||
}
|
||||
|
||||
func (h *ShareWechatHandler) writeSSEEvent(c echo.Context, data any) error {
|
||||
jsonContent, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sseMessage := fmt.Sprintf("data: %s\n\n", string(jsonContent))
|
||||
if _, err := c.Response().Write([]byte(sseMessage)); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Response().Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
// callback wechat verify
|
||||
func (h *ShareWechatHandler) VerifyUrlWechatService(c echo.Context) error {
|
||||
signature := c.QueryParam("msg_signature")
|
||||
timestamp := c.QueryParam("timestamp")
|
||||
nonce := c.QueryParam("nonce")
|
||||
echoStr := c.QueryParam("echostr")
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
if signature == "" || timestamp == "" || nonce == "" || echoStr == "" {
|
||||
return h.NewResponseWithError(
|
||||
c, "verify wechat service params failed", nil,
|
||||
)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
appInfo, err := h.appCase.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppTypeWechatServiceBot)
|
||||
|
||||
if err != nil {
|
||||
h.logger.Error("find app detail failed", log.Error(err))
|
||||
return err
|
||||
}
|
||||
if appInfo.Settings.WeChatServiceIsEnabled != nil && !*appInfo.Settings.WeChatServiceIsEnabled {
|
||||
h.logger.Error("wechat service bot is not enabled", log.Error(err))
|
||||
return errors.New("wechat service bot is not enabled")
|
||||
}
|
||||
|
||||
WechatServiceConf, err := h.wechatUsecase.NewWechatServiceConfig(ctx, kbID, appInfo)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to create WechatServiceConfig", log.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := h.wechatUsecase.VerifyUrlWechatService(ctx, signature, timestamp, nonce, echoStr, WechatServiceConf)
|
||||
if err != nil {
|
||||
h.logger.Error("VerifyURL_Service failed", log.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// success
|
||||
return c.String(http.StatusOK, string(req))
|
||||
}
|
||||
|
||||
// handler user request and sent info to wechat
|
||||
func (h *ShareWechatHandler) WechatHandlerService(c echo.Context) error {
|
||||
signature := c.QueryParam("msg_signature")
|
||||
timestamp := c.QueryParam("timestamp")
|
||||
nonce := c.QueryParam("nonce")
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
h.logger.Error("get request failed", log.Error(err))
|
||||
return err
|
||||
}
|
||||
defer c.Request().Body.Close()
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
appInfo, err := h.appCase.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppTypeWechatServiceBot)
|
||||
if err != nil {
|
||||
h.logger.Error("GetAppDetailByKBIDAndAppType failed", log.Error(err))
|
||||
return err
|
||||
}
|
||||
if appInfo.Settings.WeChatServiceIsEnabled != nil && !*appInfo.Settings.WeChatServiceIsEnabled {
|
||||
h.logger.Info("wechat service bot is not enabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建一个wechat service对象
|
||||
wechatServiceConf, err := h.wechatUsecase.NewWechatServiceConfig(context.Background(), kbID, appInfo)
|
||||
|
||||
h.logger.Info("wechat service config", log.Any("wechat service config", wechatServiceConf))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解密消息
|
||||
wxCrypt := wxbizmsgcrypt.NewWXBizMsgCrypt(wechatServiceConf.Token, wechatServiceConf.EncodingAESKey, wechatServiceConf.CorpID, wxbizmsgcrypt.XmlType)
|
||||
decryptMsg, errCode := wxCrypt.DecryptMsg(signature, timestamp, nonce, body)
|
||||
if errCode != nil {
|
||||
h.logger.Error("DecryptMsg failed", log.Any("decryptMsg err", errCode))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 反序列化
|
||||
msg, err := wechatServiceConf.UnmarshalMsg(decryptMsg)
|
||||
if err != nil {
|
||||
h.logger.Error("UnmarshalMsg failed", log.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
go func(WechatServiceConf *wechat_service.WechatServiceConfig, msg *wechat_service.WeixinUserAskMsg, kbID string) {
|
||||
ctx := context.Background()
|
||||
err := h.wechatUsecase.WechatService(ctx, msg, kbID, WechatServiceConf)
|
||||
if err != nil {
|
||||
h.logger.Error("wechat async failed", log.Any("Wechat_Service", err))
|
||||
}
|
||||
}(wechatServiceConf, msg, kbID)
|
||||
|
||||
// 先响应
|
||||
return c.JSON(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
func (h *ShareWechatHandler) VerifyUrlWechatApp(c echo.Context) error {
|
||||
signature := c.QueryParam("msg_signature")
|
||||
timestamp := c.QueryParam("timestamp")
|
||||
nonce := c.QueryParam("nonce")
|
||||
echoStr := c.QueryParam("echostr")
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
if signature == "" || timestamp == "" || nonce == "" || echoStr == "" {
|
||||
return h.NewResponseWithError(
|
||||
c, "verify wechat params failed", nil,
|
||||
)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
//1. get wechat app bot info
|
||||
appInfo, err := h.appCase.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppTypeWechatBot)
|
||||
if err != nil {
|
||||
h.logger.Error("get app detail failed", log.Error(err))
|
||||
return err
|
||||
}
|
||||
if appInfo.Settings.WeChatAppIsEnabled != nil && !*appInfo.Settings.WeChatAppIsEnabled {
|
||||
h.logger.Info("wechat service bot is not enabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
h.logger.Debug("wechat app info", log.Any("info", appInfo))
|
||||
|
||||
WechatConf, err := h.wechatAppUsecase.NewWechatConfig(ctx, appInfo, kbID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to create WechatConfig", log.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := h.wechatAppUsecase.VerifyUrlWechatAPP(ctx, signature, timestamp, nonce, echoStr, kbID, WechatConf)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "VerifyURL failed", err)
|
||||
}
|
||||
|
||||
// success
|
||||
return c.String(http.StatusOK, string(req))
|
||||
}
|
||||
|
||||
// WechatHandlerApp /share/v1/app/wechat/app
|
||||
func (h *ShareWechatHandler) WechatHandlerApp(c echo.Context) error {
|
||||
signature := c.QueryParam("msg_signature")
|
||||
timestamp := c.QueryParam("timestamp")
|
||||
nonce := c.QueryParam("nonce")
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
h.logger.Error("get request failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "Internal Server Error", err)
|
||||
}
|
||||
defer c.Request().Body.Close()
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// get appinfo and init wechatConfig
|
||||
// 查找数据库,找到对应的app配置
|
||||
appInfo, err := h.appCase.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppTypeWechatBot)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "GetAppDetailByKBIDAndAppType failed", err)
|
||||
}
|
||||
|
||||
if appInfo.Settings.WeChatAppIsEnabled != nil && !*appInfo.Settings.WeChatAppIsEnabled {
|
||||
return h.NewResponseWithError(c, "wechat app bot is not enabled", nil)
|
||||
}
|
||||
|
||||
wechatConfig, err := h.wechatAppUsecase.NewWechatConfig(context.Background(), appInfo, kbID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "wechat app config error", err)
|
||||
}
|
||||
|
||||
// 解密消息
|
||||
wxCrypt := wxbizmsgcrypt.NewWXBizMsgCrypt(wechatConfig.Token, wechatConfig.EncodingAESKey, wechatConfig.CorpID, wxbizmsgcrypt.XmlType)
|
||||
decryptMsg, errCode := wxCrypt.DecryptMsg(signature, timestamp, nonce, body)
|
||||
if errCode != nil {
|
||||
return h.NewResponseWithError(c, "DecryptMsg failed", nil)
|
||||
}
|
||||
|
||||
msg, err := wechatConfig.UnmarshalMsg(decryptMsg)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "UnmarshalMsg failed", err)
|
||||
}
|
||||
h.logger.Info("wechat app msg", log.Any("user msg", msg))
|
||||
|
||||
if msg.MsgType != "text" { // 用户进入会话,或者其他非提问类型的事件
|
||||
return c.String(http.StatusOK, "")
|
||||
}
|
||||
|
||||
var immediateResponse []byte
|
||||
if domain.GetBaseEditionLimitation(ctx).AllowAdvancedBot && appInfo.Settings.WeChatAppAdvancedSetting.TextResponseEnable {
|
||||
immediateResponse, err = wechatConfig.SendResponse(*msg, "正在思考您的问题,请稍候...")
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "Failed to send immediate response", err)
|
||||
}
|
||||
}
|
||||
|
||||
go func(ctx context.Context, msg *wechat.ReceivedMessage, wechatConfig *wechat.WechatConfig, kbId string, appInfo *domain.AppDetailResp) {
|
||||
err := h.wechatAppUsecase.Wechat(ctx, msg, wechatConfig, kbId, &appInfo.Settings.WeChatAppAdvancedSetting)
|
||||
if err != nil {
|
||||
h.logger.Error("wechat async failed")
|
||||
}
|
||||
}(ctx, msg, wechatConfig, kbID, appInfo)
|
||||
|
||||
return c.XMLBlob(http.StatusOK, immediateResponse)
|
||||
}
|
||||
|
||||
func (h *ShareWechatHandler) WecomAIBotVerify(c echo.Context) error {
|
||||
signature := c.QueryParam("msg_signature")
|
||||
timestamp := c.QueryParam("timestamp")
|
||||
nonce := c.QueryParam("nonce")
|
||||
echoStr := c.QueryParam("echostr")
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
if signature == "" || timestamp == "" || nonce == "" || echoStr == "" {
|
||||
return h.NewResponseWithError(
|
||||
c, "verify wecom ai params failed", nil,
|
||||
)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
appInfo, err := h.appCase.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppTypeWecomAIBot)
|
||||
|
||||
if err != nil {
|
||||
h.logger.Error("find app detail failed", log.Error(err))
|
||||
return err
|
||||
}
|
||||
if !appInfo.Settings.WecomAIBotSettings.IsEnabled {
|
||||
h.logger.Error("wecom ai bot is not enabled", log.Error(err))
|
||||
return errors.New("wecom ai bot is not enabled")
|
||||
}
|
||||
|
||||
resp, err := h.wecomUsecase.VerifyUrlService(ctx, signature, timestamp, nonce, echoStr, appInfo)
|
||||
if err != nil {
|
||||
h.logger.Error("wecom ai bot verify failed", log.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return c.String(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *ShareWechatHandler) WecomAIBotHandle(c echo.Context) error {
|
||||
|
||||
signature := c.QueryParam("msg_signature")
|
||||
timestamp := c.QueryParam("timestamp")
|
||||
nonce := c.QueryParam("nonce")
|
||||
|
||||
kbID := c.Request().Header.Get("X-KB-ID")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb_id is required", nil)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
h.logger.Error("get request failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "Internal Server Error", err)
|
||||
}
|
||||
defer c.Request().Body.Close()
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
appInfo, err := h.appCase.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppTypeWecomAIBot)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "GetAppDetailByKBIDAndAppType failed", err)
|
||||
}
|
||||
|
||||
if !appInfo.Settings.WecomAIBotSettings.IsEnabled {
|
||||
return h.NewResponseWithError(c, "wecom app bot is not enabled", nil)
|
||||
}
|
||||
|
||||
h.logger.Info("msg:", log.String("body", string(body)))
|
||||
resp, err := h.wecomUsecase.HandleMsg(ctx, kbID, signature, timestamp, nonce, string(body), appInfo)
|
||||
if err != nil {
|
||||
h.logger.Error("wecom ai bot handle msg failed", log.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return c.String(http.StatusOK, resp)
|
||||
}
|
||||
Reference in New Issue
Block a user