init push

This commit is contained in:
2026-05-21 19:52:45 +08:00
commit e3f75311ab
1280 changed files with 179173 additions and 0 deletions

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

View 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,
})
}

View 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,
})
}

View 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)
}

View 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 所有, 12 为需要审核的评论
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)
}

View 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,
})
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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), "*"),
)

View 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))
}

View 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)
}

View 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)
}