init push
This commit is contained in:
65
backend/handler/base.go
Normal file
65
backend/handler/base.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/chaitin/panda-wiki/config"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/middleware"
|
||||
"github.com/chaitin/panda-wiki/pkg/captcha"
|
||||
)
|
||||
|
||||
type BaseHandler struct {
|
||||
Router *echo.Echo
|
||||
baseLogger *log.Logger
|
||||
config *config.Config
|
||||
ShareAuthMiddleware *middleware.ShareAuthMiddleware
|
||||
V1Auth middleware.AuthMiddleware
|
||||
Captcha *captcha.Captcha
|
||||
}
|
||||
|
||||
func NewBaseHandler(echo *echo.Echo, logger *log.Logger, config *config.Config, v1Auth middleware.AuthMiddleware, shareAuthMiddleware *middleware.ShareAuthMiddleware, cap *captcha.Captcha) *BaseHandler {
|
||||
return &BaseHandler{
|
||||
Router: echo,
|
||||
baseLogger: logger.WithModule("http_base_handler"),
|
||||
config: config,
|
||||
ShareAuthMiddleware: shareAuthMiddleware,
|
||||
V1Auth: v1Auth,
|
||||
Captcha: cap,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BaseHandler) NewResponseWithData(c echo.Context, data any) error {
|
||||
return c.JSON(http.StatusOK, domain.PWResponse{
|
||||
Success: true,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *BaseHandler) NewResponseWithErrCode(c echo.Context, resp domain.PWResponseErrCode) error {
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *BaseHandler) NewResponseWithError(c echo.Context, msg string, err error) error {
|
||||
traceID := ""
|
||||
if h.config.GetBool("apm.enabled") {
|
||||
span := trace.SpanFromContext(c.Request().Context())
|
||||
traceID = span.SpanContext().TraceID().String()
|
||||
span.SetAttributes(attribute.String("error", fmt.Sprintf("%+v", err)), attribute.String("msg", msg))
|
||||
} else {
|
||||
traceID = uuid.New().String()
|
||||
}
|
||||
h.baseLogger.LogAttrs(c.Request().Context(), slog.LevelError, msg, slog.String("trace_id", traceID), slog.Any("error", err))
|
||||
return c.JSON(http.StatusOK, domain.PWResponse{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("%s [trace_id: %s]", msg, traceID),
|
||||
})
|
||||
}
|
||||
134
backend/handler/mq/cron.go
Normal file
134
backend/handler/mq/cron.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/repo/pg"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type CronHandler struct {
|
||||
logger *log.Logger
|
||||
statRepo *pg.StatRepository
|
||||
nodeRepo *pg.NodeRepository
|
||||
statUseCase *usecase.StatUseCase
|
||||
nodeUseCase *usecase.NodeUsecase
|
||||
}
|
||||
|
||||
func NewCronHandler(logger *log.Logger, statRepo *pg.StatRepository, nodeRepo *pg.NodeRepository, statUseCase *usecase.StatUseCase, nodeUseCase *usecase.NodeUsecase) (*CronHandler, error) {
|
||||
h := &CronHandler{
|
||||
statRepo: statRepo,
|
||||
nodeRepo: nodeRepo,
|
||||
statUseCase: statUseCase,
|
||||
nodeUseCase: nodeUseCase,
|
||||
logger: logger.WithModule("handler.mq.cron"),
|
||||
}
|
||||
cron := cron.New()
|
||||
|
||||
// 每小时 */10 分执行聚合统计数据任务
|
||||
if _, err := cron.AddFunc("*/10 */1 * * *", h.AggregateHourlyStats); err != nil {
|
||||
h.logger.Error("failed to add cron job for aggregating hourly stats", log.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
h.logger.Info("add cron job", log.String("cron_id", "aggregate_hourly_stats"))
|
||||
|
||||
// 每小时1分执行清理旧数据任务
|
||||
if _, err := cron.AddFunc("1 */1 * * *", h.RemoveOldStatData); err != nil {
|
||||
h.logger.Error("failed to add cron job for removing old data", log.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
h.logger.Info("add cron job", log.String("cron_id", "remove_old_stat_data"))
|
||||
|
||||
// 每天0点执行清理90天前的小时统计数据
|
||||
if _, err := cron.AddFunc("3 0 * * *", h.CleanupOldHourlyStats); err != nil {
|
||||
h.logger.Error("failed to add cron job for cleaning up old hourly stats", log.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
h.logger.Info("add cron job", log.String("cron_id", "cleanup_old_hourly_stats"))
|
||||
|
||||
// 启动时先异步跑一次
|
||||
go func() {
|
||||
if err := h.nodeUseCase.SyncRagNodeStatus(context.Background()); err != nil {
|
||||
h.logger.Error("initial sync rag node status failed", log.Error(err))
|
||||
}
|
||||
}()
|
||||
if _, err := cron.AddFunc("26 * * * *", h.SyncRagNodeStatus); err != nil {
|
||||
h.logger.Error("failed to sync rag node status", log.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
h.logger.Info("add cron job", log.String("cron_id", "sync_rag_node_status"))
|
||||
|
||||
// 每天2点执行清理30天前的node_release_backup数据
|
||||
if _, err := cron.AddFunc("0 2 * * *", h.CleanupOldNodeReleaseBackups); err != nil {
|
||||
h.logger.Error("failed to add cron job for cleaning up old node release backups", log.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
h.logger.Info("add cron job", log.String("cron_id", "cleanup_old_node_release_backups"))
|
||||
|
||||
cron.Start()
|
||||
h.logger.Info("start cron jobs")
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *CronHandler) RemoveOldStatData() {
|
||||
h.logger.Info("remove old stat data start")
|
||||
|
||||
// 零点时同步数据至node_stats持久化
|
||||
if time.Now().Hour() == 0 {
|
||||
if err := h.statUseCase.MigrateYesterdayPVToNodeStats(context.Background()); err != nil {
|
||||
h.logger.Error("migrate yesterday PV data to node_stats failed", log.Error(err))
|
||||
} else {
|
||||
h.logger.Info("migrate yesterday PV data to node_stats successful")
|
||||
}
|
||||
}
|
||||
|
||||
err := h.statRepo.RemoveOldData(context.Background())
|
||||
if err != nil {
|
||||
h.logger.Error("remove old stat data failed", log.Error(err))
|
||||
}
|
||||
h.logger.Info("remove old stat data successful")
|
||||
}
|
||||
|
||||
func (h *CronHandler) AggregateHourlyStats() {
|
||||
h.logger.Info("aggregate hourly stats start")
|
||||
err := h.statUseCase.AggregateHourlyStats(context.Background())
|
||||
if err != nil {
|
||||
h.logger.Error("aggregate hourly stats failed", log.Error(err))
|
||||
return
|
||||
}
|
||||
h.logger.Info("aggregate hourly stats successful")
|
||||
}
|
||||
|
||||
func (h *CronHandler) CleanupOldHourlyStats() {
|
||||
h.logger.Info("cleanup old hourly stats start")
|
||||
err := h.statUseCase.CleanupOldHourlyStats(context.Background())
|
||||
if err != nil {
|
||||
h.logger.Error("cleanup old hourly stats failed", log.Error(err))
|
||||
return
|
||||
}
|
||||
h.logger.Info("cleanup old hourly stats successful")
|
||||
}
|
||||
|
||||
func (h *CronHandler) SyncRagNodeStatus() {
|
||||
h.logger.Info("sync rag node status")
|
||||
err := h.nodeUseCase.SyncRagNodeStatus(context.Background())
|
||||
if err != nil {
|
||||
h.logger.Error("sync rag node status failed", log.Error(err))
|
||||
return
|
||||
}
|
||||
h.logger.Info("sync rag node status successful")
|
||||
}
|
||||
|
||||
func (h *CronHandler) CleanupOldNodeReleaseBackups() {
|
||||
h.logger.Info("cleanup old node release backups start")
|
||||
before := time.Now().AddDate(0, 0, -30)
|
||||
if err := h.nodeRepo.DeleteOldNodeReleaseBackups(context.Background(), before); err != nil {
|
||||
h.logger.Error("cleanup old node release backups failed", log.Error(err))
|
||||
return
|
||||
}
|
||||
h.logger.Info("cleanup old node release backups successful")
|
||||
}
|
||||
37
backend/handler/mq/provider.go
Normal file
37
backend/handler/mq/provider.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/chaitin/panda-wiki/repo/ipdb"
|
||||
"github.com/chaitin/panda-wiki/repo/mq"
|
||||
"github.com/chaitin/panda-wiki/repo/pg"
|
||||
"github.com/chaitin/panda-wiki/store/rag"
|
||||
"github.com/chaitin/panda-wiki/store/s3"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type MQHandlers struct {
|
||||
RAGMQHandler *RAGMQHandler
|
||||
RagDocUpdateHandler *RagDocUpdateHandler
|
||||
StatCronHandler *CronHandler
|
||||
}
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
pg.ProviderSet,
|
||||
rag.ProviderSet,
|
||||
mq.ProviderSet,
|
||||
ipdb.ProviderSet,
|
||||
s3.ProviderSet,
|
||||
|
||||
usecase.NewLLMUsecase,
|
||||
usecase.NewStatUseCase,
|
||||
usecase.NewNodeUsecase,
|
||||
usecase.NewModelUsecase,
|
||||
|
||||
NewRAGMQHandler,
|
||||
NewRagDocUpdateHandler,
|
||||
NewCronHandler,
|
||||
|
||||
wire.Struct(new(MQHandlers), "*"),
|
||||
)
|
||||
171
backend/handler/mq/rag.go
Normal file
171
backend/handler/mq/rag.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/mq"
|
||||
"github.com/chaitin/panda-wiki/mq/types"
|
||||
"github.com/chaitin/panda-wiki/repo/pg"
|
||||
"github.com/chaitin/panda-wiki/store/rag"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type RAGMQHandler struct {
|
||||
consumer mq.MQConsumer
|
||||
logger *log.Logger
|
||||
rag rag.RAGService
|
||||
nodeRepo *pg.NodeRepository
|
||||
kbRepo *pg.KnowledgeBaseRepository
|
||||
llmUsecase *usecase.LLMUsecase
|
||||
modelUsecase *usecase.ModelUsecase
|
||||
}
|
||||
|
||||
func NewRAGMQHandler(consumer mq.MQConsumer, logger *log.Logger, rag rag.RAGService, nodeRepo *pg.NodeRepository, kbRepo *pg.KnowledgeBaseRepository, llmUsecase *usecase.LLMUsecase, modelUsecase *usecase.ModelUsecase) (*RAGMQHandler, error) {
|
||||
h := &RAGMQHandler{
|
||||
consumer: consumer,
|
||||
logger: logger.WithModule("mq.rag"),
|
||||
rag: rag,
|
||||
nodeRepo: nodeRepo,
|
||||
kbRepo: kbRepo,
|
||||
llmUsecase: llmUsecase,
|
||||
modelUsecase: modelUsecase,
|
||||
}
|
||||
if err := consumer.RegisterHandler(domain.VectorTaskTopic, h.HandleNodeContentVectorRequest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *RAGMQHandler) HandleNodeContentVectorRequest(ctx context.Context, msg types.Message) error {
|
||||
var request domain.NodeReleaseVectorRequest
|
||||
err := json.Unmarshal(msg.GetData(), &request)
|
||||
if err != nil {
|
||||
h.logger.Error("unmarshal node content vector request failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
switch request.Action {
|
||||
case "update_group_ids":
|
||||
h.logger.Info("update node group request", log.Any("request", request), log.Any("group_id", request.GroupIds))
|
||||
kb, err := h.kbRepo.GetKnowledgeBaseByID(ctx, request.KBID)
|
||||
if err != nil {
|
||||
h.logger.Error("get kb failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
if err := h.rag.UpdateDocumentGroupIDs(ctx, kb.DatasetID, request.DocID, request.GroupIds); err != nil {
|
||||
h.logger.Error("update node group failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
h.logger.Info("update node group success", log.Any("doc_id", request.DocID), log.Any("group_ids", request.GroupIds))
|
||||
|
||||
case "upsert":
|
||||
h.logger.Debug("upsert node content vector request", "request", request)
|
||||
nodeRelease, err := h.nodeRepo.GetNodeReleaseWithDirPathByID(ctx, request.NodeReleaseID)
|
||||
if err != nil {
|
||||
h.logger.Error("get node content by ids failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
if nodeRelease.Type == domain.NodeTypeFolder {
|
||||
h.logger.Info("node is folder, skip upsert", log.Any("node_release_id", request.NodeReleaseID))
|
||||
return nil
|
||||
}
|
||||
kb, err := h.kbRepo.GetKnowledgeBaseByID(ctx, request.KBID)
|
||||
if err != nil {
|
||||
h.logger.Error("get kb failed", log.Error(err), log.String("kb_id", request.KBID))
|
||||
return nil
|
||||
}
|
||||
|
||||
groupIds, err := h.nodeRepo.GetNodeAuthGroupIdsByNodeId(ctx, nodeRelease.NodeID, consts.NodePermNameAnswerable)
|
||||
if err != nil {
|
||||
h.logger.Error("get groupIds failed", log.Error(err), log.String("kb_id", request.KBID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// upsert node content chunks
|
||||
docID, err := h.rag.UpsertRecords(ctx, &rag.UpsertRecordsRequest{
|
||||
ID: nodeRelease.ID,
|
||||
Title: nodeRelease.Name,
|
||||
DatasetID: kb.DatasetID,
|
||||
DocID: nodeRelease.DocID,
|
||||
Content: nodeRelease.Content,
|
||||
GroupIDs: groupIds,
|
||||
})
|
||||
if err != nil {
|
||||
h.logger.Error("upsert node content vector failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
// update node doc_id
|
||||
if err := h.nodeRepo.UpdateNodeReleaseDocID(ctx, request.NodeReleaseID, docID); err != nil {
|
||||
h.logger.Error("update node doc_id failed", log.String("node_id", request.NodeReleaseID), log.Error(err))
|
||||
return nil
|
||||
}
|
||||
// delete old RAG records
|
||||
// get old doc_ids by node_id
|
||||
oldDocIDs, err := h.nodeRepo.GetOldNodeDocIDsByNodeID(ctx, nodeRelease.ID, nodeRelease.NodeID)
|
||||
if err != nil {
|
||||
h.logger.Error("get old doc_ids by node_id failed", log.String("node_id", nodeRelease.NodeID), log.Error(err))
|
||||
return nil
|
||||
}
|
||||
if len(oldDocIDs) > 0 {
|
||||
// delete old RAG records
|
||||
if err := h.rag.DeleteRecords(ctx, kb.DatasetID, oldDocIDs); err != nil {
|
||||
h.logger.Error("delete old RAG records failed", log.String("kb_id", kb.ID), log.Error(err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Info("upsert node content vector success", log.Any("updated_ids", request.NodeReleaseID))
|
||||
case "delete":
|
||||
h.logger.Info("delete node content vector request", log.Any("request", request))
|
||||
kb, err := h.kbRepo.GetKnowledgeBaseByID(ctx, request.KBID)
|
||||
if err != nil {
|
||||
h.logger.Error("get kb failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
if err := h.rag.DeleteRecords(ctx, kb.DatasetID, []string{request.DocID}); err != nil {
|
||||
h.logger.Error("delete node content vector failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
h.logger.Info("delete node content vector success", log.Any("deleted_id", request.NodeReleaseID), log.Any("deleted_doc_id", request.DocID))
|
||||
case "summary":
|
||||
h.logger.Info("summary node content vector request", log.Any("request", request))
|
||||
node, err := h.nodeRepo.GetNodeByID(ctx, request.NodeID)
|
||||
if err != nil {
|
||||
h.logger.Error("get node by id failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
if node.Type == domain.NodeTypeFolder {
|
||||
h.logger.Info("node is folder, skip summary", log.Any("node_id", request.NodeID))
|
||||
return nil
|
||||
}
|
||||
|
||||
model, err := h.modelUsecase.GetChatModel(ctx)
|
||||
if err != nil {
|
||||
h.logger.Error("get chat model failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
summary, err := h.llmUsecase.SummaryNode(ctx, request.KBID, model, node.Name, node.Content)
|
||||
if err != nil {
|
||||
h.logger.Error("summary node content failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
if err := h.nodeRepo.UpdateNodeSummary(ctx, request.KBID, request.NodeID, summary); err != nil {
|
||||
h.logger.Error("update node summary failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
if node.Status == domain.NodeStatusPublished {
|
||||
if err := h.nodeRepo.UpdateNodeStatus(ctx, request.KBID, request.NodeID, domain.NodeStatusDraft); err != nil {
|
||||
h.logger.Error("update node status failed", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Info("summary node content vector success", log.Any("summary_id", request.NodeReleaseID), log.Any("summary", summary))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
67
backend/handler/mq/rag_doc_update.go
Normal file
67
backend/handler/mq/rag_doc_update.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package mq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/mq"
|
||||
"github.com/chaitin/panda-wiki/mq/types"
|
||||
"github.com/chaitin/panda-wiki/repo/pg"
|
||||
)
|
||||
|
||||
type RagDocUpdateHandler struct {
|
||||
consumer mq.MQConsumer
|
||||
logger *log.Logger
|
||||
nodeRepo *pg.NodeRepository
|
||||
}
|
||||
|
||||
func NewRagDocUpdateHandler(consumer mq.MQConsumer, logger *log.Logger, nodeRepo *pg.NodeRepository) (*RagDocUpdateHandler, error) {
|
||||
h := &RagDocUpdateHandler{
|
||||
consumer: consumer,
|
||||
logger: logger.WithModule("mq.rag_doc_update"),
|
||||
nodeRepo: nodeRepo,
|
||||
}
|
||||
if err := consumer.RegisterHandler(domain.RagDocUpdateTopic, h.HandleRagDocUpdate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *RagDocUpdateHandler) HandleRagDocUpdate(ctx context.Context, msg types.Message) error {
|
||||
var event domain.RagDocInfoUpdateEvent
|
||||
err := json.Unmarshal(msg.GetData(), &event)
|
||||
if err != nil {
|
||||
h.logger.Error("unmarshal rag doc update event failed", log.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Info("received rag doc update event",
|
||||
log.String("doc_id", event.ID),
|
||||
log.String("status", event.Status),
|
||||
log.String("message", event.Message))
|
||||
|
||||
nodeId, err := h.nodeRepo.GetNodeIdByDocId(ctx, event.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get node id by doc id",
|
||||
log.String("doc_id", event.ID),
|
||||
log.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.nodeRepo.Update(ctx, nodeId, map[string]interface{}{
|
||||
"rag_info": domain.RagInfo{
|
||||
Status: consts.NodeRagInfoStatus(event.Status),
|
||||
Message: event.Message,
|
||||
SyncedAt: time.Now(),
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.logger.Debug("node rag update success", log.String("doc_id", event.ID))
|
||||
return nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
145
backend/handler/v1/app.go
Normal file
145
backend/handler/v1/app.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/chaitin/panda-wiki/config"
|
||||
"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 AppHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
auth middleware.AuthMiddleware
|
||||
usecase *usecase.AppUsecase
|
||||
modelUsecase *usecase.ModelUsecase
|
||||
conversationUsecase *usecase.ConversationUsecase
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewAppHandler(e *echo.Echo, baseHandler *handler.BaseHandler, logger *log.Logger, auth middleware.AuthMiddleware, usecase *usecase.AppUsecase, modelUsecase *usecase.ModelUsecase, conversationUsecase *usecase.ConversationUsecase, config *config.Config) *AppHandler {
|
||||
h := &AppHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.v1.app"),
|
||||
auth: auth,
|
||||
usecase: usecase,
|
||||
modelUsecase: modelUsecase,
|
||||
conversationUsecase: conversationUsecase,
|
||||
config: config,
|
||||
}
|
||||
|
||||
group := e.Group("/api/v1/app", h.auth.Authorize)
|
||||
group.GET("/detail", h.GetAppDetail, h.auth.ValidateKBUserPerm(consts.UserKBPermissionDocManage))
|
||||
group.PUT("", h.UpdateApp, h.auth.ValidateKBUserPerm(consts.UserKBPermissionFullControl))
|
||||
group.DELETE("", h.DeleteApp, h.auth.ValidateKBUserPerm(consts.UserKBPermissionFullControl))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// GetAppDetail get app detail
|
||||
//
|
||||
// @Summary Get app detail
|
||||
// @Description Get app detail
|
||||
// @Tags app
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param kb_id query string true "kb id"
|
||||
// @Param type query string true "app type"
|
||||
// @Success 200 {object} domain.PWResponse{data=domain.AppDetailResp}
|
||||
// @Router /api/v1/app/detail [get]
|
||||
func (h *AppHandler) GetAppDetail(c echo.Context) error {
|
||||
kbID := c.QueryParam("kb_id")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb id is required", nil)
|
||||
}
|
||||
appType := c.QueryParam("type")
|
||||
if appType == "" {
|
||||
return h.NewResponseWithError(c, "type is required", nil)
|
||||
}
|
||||
appTypeInt, err := strconv.ParseInt(appType, 10, 64)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "invalid app type", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
app, err := h.usecase.GetAppDetailByKBIDAndAppType(ctx, kbID, domain.AppType(appTypeInt))
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get app detail failed", err)
|
||||
}
|
||||
if authInfo := domain.GetAuthInfoFromCtx(ctx); authInfo != nil && authInfo.Permission == consts.UserKBPermissionDocManage {
|
||||
app = h.usecase.SanitizeAppDetailForDocManage(app)
|
||||
}
|
||||
return h.NewResponseWithData(c, app)
|
||||
}
|
||||
|
||||
// UpdateApp update app
|
||||
//
|
||||
// @Summary Update app
|
||||
// @Description Update app
|
||||
// @Tags app
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param id query string true "id"
|
||||
// @Param app body domain.UpdateAppReq true "app"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/app [put]
|
||||
func (h *AppHandler) UpdateApp(c echo.Context) error {
|
||||
id := c.QueryParam("id")
|
||||
if id == "" {
|
||||
return h.NewResponseWithError(c, "id is required", nil)
|
||||
}
|
||||
|
||||
appRequest := domain.UpdateAppReq{}
|
||||
if err := c.Bind(&appRequest); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
if err := h.usecase.ValidateUpdateApp(ctx, id, &appRequest); err != nil {
|
||||
h.logger.Error("UpdateApp", log.Any("req:", appRequest.Settings), log.Any("err:", err))
|
||||
return h.NewResponseWithErrCode(c, domain.ErrCodePermissionDenied)
|
||||
}
|
||||
|
||||
if err := h.usecase.UpdateApp(ctx, id, &appRequest); err != nil {
|
||||
return h.NewResponseWithError(c, "update app failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// DeleteApp delete app
|
||||
//
|
||||
// @Summary Delete app
|
||||
// @Description Delete app
|
||||
// @Tags app
|
||||
// @Accept json
|
||||
// @Security bearerAuth
|
||||
// @Param kb_id query string true "kb id"
|
||||
// @Param id query string true "app id"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/app [delete]
|
||||
func (h *AppHandler) DeleteApp(c echo.Context) error {
|
||||
id := c.QueryParam("id")
|
||||
if id == "" {
|
||||
return h.NewResponseWithError(c, "id is required", nil)
|
||||
}
|
||||
|
||||
kbID := c.QueryParam("kb_id")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb id is required", nil)
|
||||
}
|
||||
|
||||
if err := h.usecase.DeleteApp(c.Request().Context(), id, kbID); err != nil {
|
||||
return h.NewResponseWithError(c, "delete app failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
132
backend/handler/v1/auth.go
Normal file
132
backend/handler/v1/auth.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/auth/v1"
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type AuthV1Handler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
authUseCase *usecase.AuthUsecase
|
||||
}
|
||||
|
||||
func NewAuthV1Handler(
|
||||
e *echo.Echo,
|
||||
baseHandler *handler.BaseHandler,
|
||||
logger *log.Logger,
|
||||
authUseCase *usecase.AuthUsecase,
|
||||
) *AuthV1Handler {
|
||||
h := &AuthV1Handler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger,
|
||||
authUseCase: authUseCase,
|
||||
}
|
||||
|
||||
AuthGroup := e.Group(
|
||||
"/api/v1/auth",
|
||||
h.V1Auth.Authorize,
|
||||
h.V1Auth.ValidateKBUserPerm(consts.UserKBPermissionFullControl),
|
||||
)
|
||||
AuthGroup.GET("/get", h.OpenAuthGet)
|
||||
AuthGroup.POST("/set", h.OpenAuthSet)
|
||||
AuthGroup.DELETE("/delete", h.OpenAuthDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// OpenAuthGet 获取授权信息
|
||||
//
|
||||
// @Tags Auth
|
||||
// @Summary 获取授权信息
|
||||
// @Description 获取授权信息
|
||||
// @ID v1-OpenAuthGet
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param param query v1.AuthGetReq true "para"
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.AuthGetResp}
|
||||
// @Router /api/v1/auth/get [get]
|
||||
func (h *AuthV1Handler) OpenAuthGet(c echo.Context) error {
|
||||
|
||||
var req v1.AuthGetReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
resp, err := h.authUseCase.GetAuth(c.Request().Context(), req.KBID, req.SourceType)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get Auth", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
||||
// OpenAuthSet 获取授权信息
|
||||
//
|
||||
// @Tags Auth
|
||||
// @Summary 设置授权信息
|
||||
// @Description 设置授权信息
|
||||
// @ID v1-OpenAuthSet
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param param body v1.AuthSetReq true "para"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/auth/set [post]
|
||||
func (h *AuthV1Handler) OpenAuthSet(c echo.Context) error {
|
||||
|
||||
var req v1.AuthSetReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
if err := h.authUseCase.SetAuth(c.Request().Context(), req); err != nil {
|
||||
return h.NewResponseWithError(c, "failed to set Auth", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// OpenAuthDelete 删除授权信息
|
||||
//
|
||||
// @Tags Auth
|
||||
// @Summary 删除授权信息
|
||||
// @Description 删除授权信息
|
||||
// @ID v1-OpenAuthDelete
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param param query v1.AuthDeleteReq true "para"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/auth/delete [delete]
|
||||
func (h *AuthV1Handler) OpenAuthDelete(c echo.Context) error {
|
||||
|
||||
var req v1.AuthDeleteReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
if err := h.authUseCase.DeleteAuth(c.Request().Context(), req); err != nil {
|
||||
return h.NewResponseWithError(c, "failed to delete Auth", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
92
backend/handler/v1/comment.go
Normal file
92
backend/handler/v1/comment.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"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 CommentHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
auth middleware.AuthMiddleware
|
||||
usecase *usecase.CommentUsecase
|
||||
}
|
||||
|
||||
func NewCommentHandler(e *echo.Echo, baseHandler *handler.BaseHandler, logger *log.Logger, auth middleware.AuthMiddleware,
|
||||
usecase *usecase.CommentUsecase) *CommentHandler {
|
||||
h := &CommentHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.v1.comment"),
|
||||
auth: auth,
|
||||
usecase: usecase,
|
||||
}
|
||||
|
||||
group := e.Group("/api/v1/comment", h.auth.Authorize, h.auth.ValidateKBUserPerm(consts.UserKBPermissionDataOperate))
|
||||
group.GET("", h.GetCommentModeratedList)
|
||||
group.DELETE("/list", h.DeleteCommentList)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type CommentLists = domain.PaginatedResult[[]*domain.CommentListItem]
|
||||
|
||||
// GetCommentModeratedList
|
||||
//
|
||||
// @Summary GetCommentModeratedList
|
||||
// @Description GetCommentModeratedList
|
||||
// @Tags comment
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param req query domain.CommentListReq true "CommentListReq"
|
||||
// @Success 200 {object} domain.PWResponse{data=CommentLists} "conversationList"
|
||||
// @Router /api/v1/comment [get]
|
||||
func (h *CommentHandler) GetCommentModeratedList(c echo.Context) error {
|
||||
var req domain.CommentListReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "bind request", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
commentList, err := h.usecase.GetCommentListByKbID(ctx, &req, consts.GetLicenseEdition(c))
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get comment list KBID", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, commentList)
|
||||
}
|
||||
|
||||
// DeleteCommentList
|
||||
//
|
||||
// @Summary DeleteCommentList
|
||||
// @Description DeleteCommentList
|
||||
// @Tags comment
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param req query domain.DeleteCommentListReq true "DeleteCommentListReq"
|
||||
// @Success 200 {object} domain.Response "total"
|
||||
// @Router /api/v1/comment/list [delete]
|
||||
func (h *CommentHandler) DeleteCommentList(c echo.Context) error {
|
||||
var req domain.DeleteCommentListReq
|
||||
ids := c.QueryParams()["ids[]"]
|
||||
if len(ids) == 0 {
|
||||
return h.NewResponseWithError(c, "len comment id is zero", nil)
|
||||
}
|
||||
req.IDS = ids
|
||||
ctx := c.Request().Context()
|
||||
err := h.usecase.DeleteCommentList(ctx, &req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to delete comment list", err)
|
||||
}
|
||||
|
||||
// success
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
144
backend/handler/v1/conversation.go
Normal file
144
backend/handler/v1/conversation.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/conversation/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 ConversationHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
auth middleware.AuthMiddleware
|
||||
usecase *usecase.ConversationUsecase
|
||||
}
|
||||
|
||||
func NewConversationHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *log.Logger, auth middleware.AuthMiddleware, usecase *usecase.ConversationUsecase) *ConversationHandler {
|
||||
handler := &ConversationHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler_conversation"),
|
||||
auth: auth,
|
||||
usecase: usecase,
|
||||
}
|
||||
group := echo.Group("/api/v1/conversation", handler.auth.Authorize, handler.auth.ValidateKBUserPerm(consts.UserKBPermissionDataOperate))
|
||||
group.GET("", handler.GetConversationList)
|
||||
group.GET("/detail", handler.GetConversationDetail)
|
||||
group.GET("/message/list", handler.GetMessageFeedBackList)
|
||||
group.GET("/message/detail", handler.GetMessageDetail)
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
type ConversationListItems = domain.PaginatedResult[[]domain.ConversationListItem]
|
||||
|
||||
// GetConversationList
|
||||
//
|
||||
// @Summary get conversation list
|
||||
// @Description get conversation list
|
||||
// @Tags conversation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param req query domain.ConversationListReq true "conversation list request"
|
||||
// @Success 200 {object} domain.PWResponse{data=ConversationListItems}
|
||||
// @Router /api/v1/conversation [get]
|
||||
func (h *ConversationHandler) GetConversationList(c echo.Context) error {
|
||||
var request domain.ConversationListReq
|
||||
if err := c.Bind(&request); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
conversationList, err := h.usecase.GetConversationList(ctx, &request)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get conversation list", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, conversationList)
|
||||
}
|
||||
|
||||
// GetConversationDetail
|
||||
//
|
||||
// @Summary get conversation detail
|
||||
// @Description get conversation detail
|
||||
// @Tags conversation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param query v1.GetConversationDetailReq true "conversation id"
|
||||
// @Success 200 {object} domain.PWResponse{data=domain.ConversationDetailResp}
|
||||
// @Router /api/v1/conversation/detail [get]
|
||||
func (h *ConversationHandler) GetConversationDetail(c echo.Context) error {
|
||||
|
||||
var req v1.GetConversationDetailReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
conversation, err := h.usecase.GetConversationDetail(c.Request().Context(), req.KbId, req.ID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get conversation detail", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, conversation)
|
||||
}
|
||||
|
||||
// GetMessageFeedBackList
|
||||
//
|
||||
// @Summary GetMessageFeedBackList
|
||||
// @Description GetMessageFeedBackList
|
||||
// @Tags Message
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param req query domain.MessageListReq true "message list request"
|
||||
//
|
||||
// @Success 200 {object} domain.PWResponse{data=domain.PaginatedResult[[]domain.ConversationMessageListItem]} "MessageList"
|
||||
// @Router /api/v1/conversation/message/list [get]
|
||||
func (h *ConversationHandler) GetMessageFeedBackList(c echo.Context) error {
|
||||
var request domain.MessageListReq
|
||||
if err := c.Bind(&request); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
h.logger.Info("GetMessageFeedBackList request", log.Any("request", request))
|
||||
ctx := c.Request().Context()
|
||||
messages, err := h.usecase.GetMessageList(ctx, &request)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get message list", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, messages)
|
||||
}
|
||||
|
||||
// GetMessageDetail
|
||||
//
|
||||
// @Summary Get message detail
|
||||
// @Description Get message detail
|
||||
// @Tags Message
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query v1.GetMessageDetailReq true "message id"
|
||||
// @Success 200 {object} domain.PWResponse{data=domain.ConversationMessage}
|
||||
// @Router /api/v1/conversation/message/detail [get]
|
||||
func (h *ConversationHandler) GetMessageDetail(c echo.Context) error {
|
||||
var req v1.GetMessageDetailReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
message, err := h.usecase.GetMessageDetail(c.Request().Context(), req.KbId, req.ID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get message detail", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, message)
|
||||
}
|
||||
164
backend/handler/v1/crawler.go
Normal file
164
backend/handler/v1/crawler.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/crawler/v1"
|
||||
"github.com/chaitin/panda-wiki/config"
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"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 CrawlerHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
usecase *usecase.CrawlerUsecase
|
||||
config *config.Config
|
||||
fileUsecase *usecase.FileUsecase
|
||||
}
|
||||
|
||||
func NewCrawlerHandler(echo *echo.Echo,
|
||||
baseHandler *handler.BaseHandler,
|
||||
auth middleware.AuthMiddleware,
|
||||
logger *log.Logger,
|
||||
config *config.Config,
|
||||
usecase *usecase.CrawlerUsecase,
|
||||
fileUsecase *usecase.FileUsecase,
|
||||
) *CrawlerHandler {
|
||||
h := &CrawlerHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.v1.crawler"),
|
||||
config: config,
|
||||
usecase: usecase,
|
||||
fileUsecase: fileUsecase,
|
||||
}
|
||||
group := echo.Group("/api/v1/crawler", auth.Authorize)
|
||||
group.POST("/parse", h.CrawlerParse)
|
||||
group.POST("/export", h.CrawlerExport)
|
||||
group.GET("/result", h.CrawlerResult)
|
||||
group.POST("/results", h.CrawlerResults)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// CrawlerParse 解析文档树
|
||||
//
|
||||
// @Summary 解析文档树
|
||||
// @Description 解析文档树
|
||||
// @Tags crawler
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body v1.CrawlerParseReq true "Scrape"
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.CrawlerParseResp}
|
||||
// @Router /api/v1/crawler/parse [post]
|
||||
func (h *CrawlerHandler) CrawlerParse(c echo.Context) error {
|
||||
var req v1.CrawlerParseReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
|
||||
switch req.CrawlerSource {
|
||||
case consts.CrawlerSourceFeishu:
|
||||
if req.FeishuSetting.AppID == "" || req.FeishuSetting.AppSecret == "" || req.FeishuSetting.UserAccessToken == "" {
|
||||
return h.NewResponseWithError(c, "validate request param feishu failed", nil)
|
||||
}
|
||||
case consts.CrawlerSourceDingtalk:
|
||||
if req.DingtalkSetting.AppID == "" || req.DingtalkSetting.AppSecret == "" || (req.DingtalkSetting.UnionID == "" && req.DingtalkSetting.Phone == "") {
|
||||
return h.NewResponseWithError(c, "validate request param dingtalk failed", nil)
|
||||
}
|
||||
default:
|
||||
if req.Key == "" {
|
||||
return h.NewResponseWithError(c, "validate request param key failed", nil)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := h.usecase.ParseUrl(c.Request().Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("scrape url failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "scrape url failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
||||
// CrawlerExport
|
||||
//
|
||||
// @Summary CrawlerExport
|
||||
// @Description CrawlerExport
|
||||
// @Tags crawler
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body v1.CrawlerExportReq true "Scrape"
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.CrawlerExportResp}
|
||||
// @Router /api/v1/crawler/export [post]
|
||||
func (h *CrawlerHandler) CrawlerExport(c echo.Context) error {
|
||||
var req v1.CrawlerExportReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
resp, err := h.usecase.ExportDoc(c.Request().Context(), &req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "scrape url failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
||||
// CrawlerResult
|
||||
//
|
||||
// @Summary Get Crawler Result
|
||||
// @Description Retrieve the result of a previously started scraping task
|
||||
// @Tags crawler
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body v1.CrawlerResultReq true "Crawler Result Request"
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.CrawlerResultResp}
|
||||
// @Router /api/v1/crawler/result [get]
|
||||
func (h *CrawlerHandler) CrawlerResult(c echo.Context) error {
|
||||
var req v1.CrawlerResultReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request params is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
resp, err := h.usecase.ScrapeGetResult(c.Request().Context(), req.TaskId)
|
||||
if err != nil {
|
||||
h.logger.Error("get scrape result failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "get scrape result failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
||||
// CrawlerResults
|
||||
//
|
||||
// @Summary Get Crawler Results
|
||||
// @Description Retrieve the results of a previously started scraping task
|
||||
// @Tags crawler
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param param body v1.CrawlerResultsReq true "Crawler Results Request"
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.CrawlerResultsResp}
|
||||
// @Router /api/v1/crawler/results [post]
|
||||
func (h *CrawlerHandler) CrawlerResults(c echo.Context) error {
|
||||
var req v1.CrawlerResultsReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request params is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
resp, err := h.usecase.ScrapeGetResults(c.Request().Context(), req.TaskIds)
|
||||
if err != nil {
|
||||
h.logger.Error("get scrape results failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "get scrape results failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
103
backend/handler/v1/creation.go
Normal file
103
backend/handler/v1/creation.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"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 CreationHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
usecase *usecase.CreationUsecase
|
||||
}
|
||||
|
||||
func NewCreationHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *log.Logger, usecase *usecase.CreationUsecase) *CreationHandler {
|
||||
h := &CreationHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.v1.creation"),
|
||||
usecase: usecase,
|
||||
}
|
||||
|
||||
api := echo.Group("/api/v1/creation", h.V1Auth.Authorize)
|
||||
api.POST("/text", h.Text)
|
||||
api.POST("/tab-complete", h.TabComplete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// Text text creation
|
||||
//
|
||||
// @Summary Text creation
|
||||
// @Description Text creation
|
||||
// @Tags creation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body domain.TextReq true "text creation request"
|
||||
// @Success 200 {string} string "success"
|
||||
// @Router /api/v1/creation/text [post]
|
||||
func (h *CreationHandler) Text(c echo.Context) error {
|
||||
var req domain.TextReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
onChunk := func(ctx context.Context, dataType, chunk string) error {
|
||||
if _, err := c.Response().Write([]byte(chunk)); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Response().Flush()
|
||||
return nil
|
||||
}
|
||||
err := h.usecase.TextCreation(c.Request().Context(), &req, onChunk)
|
||||
if err != nil {
|
||||
h.logger.Error("text creation failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "text creation failed", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TabComplete handles tab-based document completion similar to AI coding's FIM (Fill in Middle)
|
||||
//
|
||||
// @Summary Tab-based document completion
|
||||
// @Description Tab-based document completion similar to AI coding's FIM (Fill in Middle)
|
||||
// @Tags creation
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body domain.CompleteReq true "tab completion request"
|
||||
// @Success 200 {string} string "success"
|
||||
// @Router /api/v1/creation/tab-complete [post]
|
||||
func (h *CreationHandler) TabComplete(c echo.Context) error {
|
||||
var req domain.CompleteReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
|
||||
// For FIM-style completion, we don't need streaming
|
||||
result, err := h.usecase.TabComplete(c.Request().Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("tab completion failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "tab completion failed", err)
|
||||
}
|
||||
|
||||
return c.JSON(200, map[string]interface{}{
|
||||
"success": true,
|
||||
"data": result,
|
||||
})
|
||||
}
|
||||
159
backend/handler/v1/file.go
Normal file
159
backend/handler/v1/file.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/chaitin/panda-wiki/config"
|
||||
"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/store/s3"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
"github.com/chaitin/panda-wiki/utils"
|
||||
)
|
||||
|
||||
type FileHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
auth middleware.AuthMiddleware
|
||||
config *config.Config
|
||||
fileUsecase *usecase.FileUsecase
|
||||
}
|
||||
|
||||
func NewFileHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *log.Logger, auth middleware.AuthMiddleware, minioClient *s3.MinioClient, config *config.Config, fileUsecase *usecase.FileUsecase) *FileHandler {
|
||||
h := &FileHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.v1.file"),
|
||||
auth: auth,
|
||||
config: config,
|
||||
fileUsecase: fileUsecase,
|
||||
}
|
||||
group := echo.Group("/api/v1/file")
|
||||
group.POST("/upload", h.Upload, h.auth.Authorize)
|
||||
group.POST("/upload/url", h.UploadByUrl, h.auth.Authorize)
|
||||
group.POST("/upload/anydoc", h.UploadAnydoc)
|
||||
return h
|
||||
}
|
||||
|
||||
// Upload
|
||||
//
|
||||
// @Summary Upload File
|
||||
// @Description Upload File
|
||||
// @Tags file
|
||||
// @Accept multipart/form-data
|
||||
// @Param file formData file true "File"
|
||||
// @Param kb_id formData string false "Knowledge Base ID"
|
||||
// @Success 200 {object} domain.ObjectUploadResp
|
||||
// @Router /api/v1/file/upload [post]
|
||||
func (h *FileHandler) Upload(c echo.Context) error {
|
||||
cxt := c.Request().Context()
|
||||
kbID := c.FormValue("kb_id")
|
||||
if kbID == "" {
|
||||
kbID = uuid.New().String()
|
||||
}
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get file", err)
|
||||
}
|
||||
key, err := h.fileUsecase.UploadFile(cxt, kbID, file)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "upload failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, domain.ObjectUploadResp{
|
||||
Key: key,
|
||||
Filename: file.Filename,
|
||||
})
|
||||
}
|
||||
|
||||
// UploadByUrl
|
||||
//
|
||||
// @Summary Upload File By Url
|
||||
// @Description Upload File By Url
|
||||
// @Tags file
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body domain.UploadByUrlReq true "Request Body"
|
||||
// @Success 200 {object} domain.Response{data=domain.ObjectUploadResp}
|
||||
// @Router /api/v1/file/upload/url [post]
|
||||
func (h *FileHandler) UploadByUrl(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var req domain.UploadByUrlReq
|
||||
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 := req.KbId
|
||||
if kbID == "" {
|
||||
kbID = uuid.New().String()
|
||||
}
|
||||
|
||||
key, err := h.fileUsecase.UploadFileByUrl(ctx, kbID, req.Url)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "upload failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, domain.ObjectUploadResp{
|
||||
Key: key,
|
||||
})
|
||||
}
|
||||
|
||||
// UploadAnydoc
|
||||
//
|
||||
// @Summary Upload Anydoc File
|
||||
// @Description Upload Anydoc File
|
||||
// @Tags file
|
||||
// @Accept multipart/form-data
|
||||
// @Param file formData file true "File"
|
||||
// @Param path formData string true "File Path"
|
||||
// @Success 200 {object} domain.AnydocUploadResp
|
||||
// @Router /api/v1/file/upload/anydoc [post]
|
||||
func (h *FileHandler) UploadAnydoc(c echo.Context) error {
|
||||
clientIP := fmt.Sprintf("%s.17", h.config.SubnetPrefix)
|
||||
if utils.GetClientIPFromRemoteAddr(c) != clientIP {
|
||||
return c.JSON(http.StatusUnauthorized, domain.AnydocUploadResp{
|
||||
Code: 1,
|
||||
Err: "invalid required",
|
||||
})
|
||||
}
|
||||
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, domain.AnydocUploadResp{
|
||||
Code: 1,
|
||||
Err: "invalid required",
|
||||
})
|
||||
}
|
||||
|
||||
path := c.FormValue("path")
|
||||
if path == "" {
|
||||
return c.JSON(http.StatusBadRequest, domain.AnydocUploadResp{
|
||||
Code: 1,
|
||||
Err: "invalid required",
|
||||
})
|
||||
}
|
||||
|
||||
h.logger.Debug("AnydocUpload file", "path", path)
|
||||
_, err = h.fileUsecase.AnyDocUploadFile(c.Request().Context(), file, path)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "upload failed", err)
|
||||
}
|
||||
url := fmt.Sprintf("/static-file/%s", strings.TrimPrefix(path, "/"))
|
||||
h.logger.Debug("AnydocUpload file", "path", url)
|
||||
|
||||
return c.JSON(http.StatusOK, domain.AnydocUploadResp{
|
||||
Code: 0,
|
||||
Data: url,
|
||||
})
|
||||
}
|
||||
130
backend/handler/v1/kb_user.go
Normal file
130
backend/handler/v1/kb_user.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/kb/v1"
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
)
|
||||
|
||||
// KBUserList
|
||||
//
|
||||
// @Summary KBUserList
|
||||
// @Description KBUserList
|
||||
// @Tags knowledge_base
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param kb_id query string true "Knowledge Base ID"
|
||||
// @Success 200 {object} domain.PWResponse{data=[]v1.KBUserListItemResp}
|
||||
// @Router /api/v1/knowledge_base/user/list [get]
|
||||
func (h *KnowledgeBaseHandler) KBUserList(c echo.Context) error {
|
||||
var req v1.KBUserListReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request params is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
resp, err := h.usecase.GetKBUserList(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get kb user list failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
||||
// KBUserInvite
|
||||
//
|
||||
// @Summary KBUserInvite
|
||||
// @Description Invite user to knowledge base
|
||||
// @Tags knowledge_base
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param param body v1.KBUserInviteReq true "Invite User Request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/knowledge_base/user/invite [post]
|
||||
func (h *KnowledgeBaseHandler) KBUserInvite(c echo.Context) error {
|
||||
var req v1.KBUserInviteReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
|
||||
}
|
||||
|
||||
err := h.usecase.KBUserInvite(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "invite user to kb failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// KBUserUpdate
|
||||
//
|
||||
// @Summary KBUserUpdate
|
||||
// @Description Update user permission in knowledge base
|
||||
// @Tags knowledge_base
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param param body v1.KBUserUpdateReq true "Update User Permission Request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/knowledge_base/user/update [patch]
|
||||
func (h *KnowledgeBaseHandler) KBUserUpdate(c echo.Context) error {
|
||||
var req v1.KBUserUpdateReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
|
||||
}
|
||||
|
||||
err := h.usecase.UpdateUserKB(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "update user kb permission failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// KBUserDelete
|
||||
//
|
||||
// @Summary KBUserDelete
|
||||
// @Description Remove user from knowledge base
|
||||
// @Tags knowledge_base
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param param query v1.KBUserDeleteReq true "Remove User Request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/knowledge_base/user/delete [delete]
|
||||
func (h *KnowledgeBaseHandler) KBUserDelete(c echo.Context) error {
|
||||
var req v1.KBUserDeleteReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
err := h.usecase.KBUserDelete(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "remove user from kb failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
292
backend/handler/v1/knowledge_base.go
Normal file
292
backend/handler/v1/knowledge_base.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"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 KnowledgeBaseHandler struct {
|
||||
*handler.BaseHandler
|
||||
usecase *usecase.KnowledgeBaseUsecase
|
||||
llmUsecase *usecase.LLMUsecase
|
||||
logger *log.Logger
|
||||
auth middleware.AuthMiddleware
|
||||
}
|
||||
|
||||
func NewKnowledgeBaseHandler(
|
||||
baseHandler *handler.BaseHandler,
|
||||
echo *echo.Echo,
|
||||
usecase *usecase.KnowledgeBaseUsecase,
|
||||
llmUsecase *usecase.LLMUsecase,
|
||||
auth middleware.AuthMiddleware,
|
||||
logger *log.Logger,
|
||||
) *KnowledgeBaseHandler {
|
||||
h := &KnowledgeBaseHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.v1.knowledge_base"),
|
||||
usecase: usecase,
|
||||
llmUsecase: llmUsecase,
|
||||
auth: auth,
|
||||
}
|
||||
|
||||
group := echo.Group("/api/v1/knowledge_base", h.auth.Authorize)
|
||||
group.POST("", h.CreateKnowledgeBase, h.auth.ValidateUserRole(consts.UserRoleAdmin))
|
||||
group.GET("/list", h.GetKnowledgeBaseList)
|
||||
group.GET("/detail", h.GetKnowledgeBaseDetail, h.auth.ValidateKBUserPerm(consts.UserKBPermissionNotNull))
|
||||
group.PUT("/detail", h.UpdateKnowledgeBase, h.auth.ValidateKBUserPerm(consts.UserKBPermissionFullControl))
|
||||
group.DELETE("/detail", h.DeleteKnowledgeBase, h.auth.ValidateUserRole(consts.UserRoleAdmin))
|
||||
|
||||
// user management
|
||||
userGroup := group.Group("/user", h.auth.ValidateKBUserPerm(consts.UserKBPermissionFullControl))
|
||||
userGroup.GET("/list", h.KBUserList)
|
||||
userGroup.POST("/invite", h.KBUserInvite)
|
||||
userGroup.PATCH("/update", h.KBUserUpdate)
|
||||
userGroup.DELETE("/delete", h.KBUserDelete)
|
||||
|
||||
// release
|
||||
releaseGroup := group.Group("/release", h.auth.ValidateKBUserPerm(consts.UserKBPermissionDocManage))
|
||||
releaseGroup.POST("", h.CreateKBRelease)
|
||||
releaseGroup.GET("/list", h.GetKBReleaseList)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// CreateKnowledgeBase
|
||||
//
|
||||
// @Summary CreateKnowledgeBase
|
||||
// @Description CreateKnowledgeBase
|
||||
// @Tags knowledge_base
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body domain.CreateKnowledgeBaseReq true "CreateKnowledgeBase Request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/knowledge_base [post]
|
||||
func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c echo.Context) error {
|
||||
|
||||
var req domain.CreateKnowledgeBaseReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
req.Hosts = lo.Uniq(req.Hosts)
|
||||
req.Ports = lo.Uniq(req.Ports)
|
||||
req.SSLPorts = lo.Uniq(req.SSLPorts)
|
||||
|
||||
if len(req.Hosts) == 0 {
|
||||
return h.NewResponseWithError(c, "hosts is required", nil)
|
||||
}
|
||||
if len(req.Ports)+len(req.SSLPorts) == 0 {
|
||||
return h.NewResponseWithError(c, "ports is required", nil)
|
||||
}
|
||||
|
||||
req.MaxKB = domain.GetBaseEditionLimitation(c.Request().Context()).MaxKb
|
||||
|
||||
did, err := h.usecase.CreateKnowledgeBase(c.Request().Context(), &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrPortHostAlreadyExists) {
|
||||
return h.NewResponseWithError(c, "端口或域名已被其他知识库占用", nil)
|
||||
}
|
||||
if errors.Is(err, domain.ErrSyncCaddyConfigFailed) {
|
||||
return h.NewResponseWithError(c, "保存配置失败,请检查端口或证书配置", nil)
|
||||
}
|
||||
return h.NewResponseWithError(c, "failed to create knowledge base", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, map[string]string{
|
||||
"id": did,
|
||||
})
|
||||
}
|
||||
|
||||
// GetKnowledgeBaseList
|
||||
//
|
||||
// @Summary GetKnowledgeBaseList
|
||||
// @Description GetKnowledgeBaseList
|
||||
// @Tags knowledge_base
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.PWResponse{data=[]domain.KnowledgeBaseListItem}
|
||||
// @Router /api/v1/knowledge_base/list [get]
|
||||
func (h *KnowledgeBaseHandler) GetKnowledgeBaseList(c echo.Context) error {
|
||||
|
||||
knowledgeBases, err := h.usecase.GetKnowledgeBaseListByUserId(c.Request().Context())
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get knowledge base list", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, knowledgeBases)
|
||||
}
|
||||
|
||||
// UpdateKnowledgeBase
|
||||
//
|
||||
// @Summary UpdateKnowledgeBase
|
||||
// @Description UpdateKnowledgeBase
|
||||
// @Tags knowledge_base
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body domain.UpdateKnowledgeBaseReq true "UpdateKnowledgeBase Request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/knowledge_base/detail [put]
|
||||
func (h *KnowledgeBaseHandler) UpdateKnowledgeBase(c echo.Context) error {
|
||||
var req domain.UpdateKnowledgeBaseReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
err := h.usecase.UpdateKnowledgeBase(c.Request().Context(), &req)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrPortHostAlreadyExists) {
|
||||
return h.NewResponseWithError(c, "端口或域名已被其他知识库占用", nil)
|
||||
}
|
||||
if errors.Is(err, domain.ErrSyncCaddyConfigFailed) {
|
||||
return h.NewResponseWithError(c, "保存配置失败,请检查端口或证书配置", nil)
|
||||
}
|
||||
return h.NewResponseWithError(c, "failed to update knowledge base", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// GetKnowledgeBaseDetail
|
||||
//
|
||||
// @Summary GetKnowledgeBaseDetail
|
||||
// @Description GetKnowledgeBaseDetail
|
||||
// @Tags knowledge_base
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param id query string true "Knowledge Base ID"
|
||||
// @Success 200 {object} domain.PWResponse{data=domain.KnowledgeBaseDetail}
|
||||
// @Router /api/v1/knowledge_base/detail [get]
|
||||
func (h *KnowledgeBaseHandler) GetKnowledgeBaseDetail(c echo.Context) error {
|
||||
kbID := c.QueryParam("id")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb id is required", nil)
|
||||
}
|
||||
|
||||
kb, err := h.usecase.GetKnowledgeBase(c.Request().Context(), kbID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get knowledge base detail", err)
|
||||
}
|
||||
|
||||
perm, err := h.usecase.GetKnowledgeBasePerm(c.Request().Context(), kbID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get knowledge base permission", err)
|
||||
}
|
||||
|
||||
if perm != consts.UserKBPermissionFullControl {
|
||||
kb.AccessSettings.PrivateKey = ""
|
||||
kb.AccessSettings.PublicKey = ""
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, &domain.KnowledgeBaseDetail{
|
||||
ID: kb.ID,
|
||||
Name: kb.Name,
|
||||
DatasetID: kb.DatasetID,
|
||||
Perm: perm,
|
||||
AccessSettings: kb.AccessSettings,
|
||||
CreatedAt: kb.CreatedAt,
|
||||
UpdatedAt: kb.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteKnowledgeBase
|
||||
//
|
||||
// @Summary DeleteKnowledgeBase
|
||||
// @Description DeleteKnowledgeBase
|
||||
// @Tags knowledge_base
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query string true "Knowledge Base ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/knowledge_base/detail [delete]
|
||||
func (h *KnowledgeBaseHandler) DeleteKnowledgeBase(c echo.Context) error {
|
||||
kbID := c.QueryParam("id")
|
||||
if kbID == "" {
|
||||
return h.NewResponseWithError(c, "kb id is required", nil)
|
||||
}
|
||||
|
||||
err := h.usecase.DeleteKnowledgeBase(c.Request().Context(), kbID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to delete knowledge base", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// CreateKBRelease
|
||||
//
|
||||
// @Summary CreateKBRelease
|
||||
// @Description CreateKBRelease
|
||||
// @Tags knowledge_base
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body domain.CreateKBReleaseReq true "CreateKBRelease Request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/knowledge_base/release [post]
|
||||
func (h *KnowledgeBaseHandler) CreateKBRelease(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
authInfo := domain.GetAuthInfoFromCtx(ctx)
|
||||
if authInfo == nil {
|
||||
return h.NewResponseWithError(c, "authInfo not found in context", nil)
|
||||
}
|
||||
|
||||
req := &domain.CreateKBReleaseReq{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
|
||||
id, err := h.usecase.CreateKBRelease(ctx, req, authInfo.UserId)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "create kb release failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, map[string]any{
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
// GetKBReleaseList
|
||||
//
|
||||
// @Summary GetKBReleaseList
|
||||
// @Description GetKBReleaseList
|
||||
// @Tags knowledge_base
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param kb_id query string true "Knowledge Base ID"
|
||||
// @Success 200 {object} domain.PWResponse{data=domain.GetKBReleaseListResp}
|
||||
// @Router /api/v1/knowledge_base/release/list [get]
|
||||
func (h *KnowledgeBaseHandler) GetKBReleaseList(c echo.Context) error {
|
||||
var req domain.GetKBReleaseListReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request params is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
resp, err := h.usecase.GetKBReleaseList(c.Request().Context(), &req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get kb release list failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
269
backend/handler/v1/model.go
Normal file
269
backend/handler/v1/model.go
Normal file
@@ -0,0 +1,269 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
modelkitDomain "github.com/chaitin/ModelKit/v2/domain"
|
||||
modelkit "github.com/chaitin/ModelKit/v2/usecase"
|
||||
|
||||
"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 ModelHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
auth middleware.AuthMiddleware
|
||||
usecase *usecase.ModelUsecase
|
||||
llmUsecase *usecase.LLMUsecase
|
||||
modelkit *modelkit.ModelKit
|
||||
}
|
||||
|
||||
func NewModelHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *log.Logger, auth middleware.AuthMiddleware, usecase *usecase.ModelUsecase, llmUsecase *usecase.LLMUsecase) *ModelHandler {
|
||||
modelkit := modelkit.NewModelKit(logger.Logger)
|
||||
handler := &ModelHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.v1.model"),
|
||||
auth: auth,
|
||||
usecase: usecase,
|
||||
llmUsecase: llmUsecase,
|
||||
modelkit: modelkit,
|
||||
}
|
||||
group := echo.Group("/api/v1/model", handler.auth.Authorize, handler.auth.ValidateUserRole(consts.UserRoleAdmin))
|
||||
group.GET("/list", handler.GetModelList)
|
||||
group.POST("", handler.CreateModel)
|
||||
group.POST("/check", handler.CheckModel)
|
||||
group.POST("/provider/supported", handler.GetProviderSupportedModelList)
|
||||
group.PUT("", handler.UpdateModel)
|
||||
group.POST("/switch-mode", handler.SwitchMode)
|
||||
group.GET("/mode-setting", handler.GetModelModeSetting)
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// GetModelList
|
||||
//
|
||||
// @Summary get model list
|
||||
// @Description get model list
|
||||
// @Tags model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.PWResponse{data=domain.ModelListItem}
|
||||
// @Router /api/v1/model/list [get]
|
||||
func (h *ModelHandler) GetModelList(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
models, err := h.usecase.GetList(ctx)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get model list failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, models)
|
||||
}
|
||||
|
||||
// CreateModel
|
||||
//
|
||||
// @Summary create model
|
||||
// @Description create model
|
||||
// @Tags model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param model body domain.CreateModelReq true "create model request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/model [post]
|
||||
func (h *ModelHandler) CreateModel(c echo.Context) error {
|
||||
var req domain.CreateModelReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
param := domain.ModelParam{}
|
||||
if req.Parameters != nil {
|
||||
param = *req.Parameters
|
||||
}
|
||||
model := &domain.Model{
|
||||
ID: uuid.New().String(),
|
||||
Provider: req.Provider,
|
||||
Model: req.Model,
|
||||
APIKey: req.APIKey,
|
||||
APIHeader: req.APIHeader,
|
||||
BaseURL: req.BaseURL,
|
||||
APIVersion: req.APIVersion,
|
||||
Type: req.Type,
|
||||
IsActive: true,
|
||||
Parameters: param,
|
||||
}
|
||||
if err := h.usecase.Create(ctx, model); err != nil {
|
||||
return h.NewResponseWithError(c, "create model failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, model)
|
||||
}
|
||||
|
||||
// UpdateModel
|
||||
//
|
||||
// @Description update model
|
||||
// @Tags model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param model body domain.UpdateModelReq true "update model request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/model [put]
|
||||
func (h *ModelHandler) UpdateModel(c echo.Context) error {
|
||||
var req domain.UpdateModelReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
// 不支持修改非视觉模型的启用状态
|
||||
if req.IsActive != nil && req.Type != domain.ModelTypeAnalysisVL {
|
||||
return h.NewResponseWithError(c, "仅支持修改视觉模型的启用状态", nil)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
if err := h.usecase.Update(ctx, &req); err != nil {
|
||||
return h.NewResponseWithError(c, "update model failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// CheckModel
|
||||
//
|
||||
// @Summary check model
|
||||
// @Description check model
|
||||
// @Tags model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param model body domain.CheckModelReq true "check model request"
|
||||
// @Success 200 {object} domain.Response{data=domain.CheckModelResp}
|
||||
// @Router /api/v1/model/check [post]
|
||||
func (h *ModelHandler) CheckModel(c echo.Context) error {
|
||||
var req domain.CheckModelReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
modelType := req.Type
|
||||
switch req.Type {
|
||||
case domain.ModelTypeAnalysis, domain.ModelTypeAnalysisVL: // for rag analysis
|
||||
modelType = domain.ModelTypeChat
|
||||
default:
|
||||
}
|
||||
model, err := h.modelkit.CheckModel(ctx, &modelkitDomain.CheckModelReq{
|
||||
Provider: string(req.Provider),
|
||||
Model: req.Model,
|
||||
BaseURL: req.BaseURL,
|
||||
APIKey: req.APIKey,
|
||||
APIHeader: req.APIHeader,
|
||||
APIVersion: req.APIVersion,
|
||||
Type: string(modelType),
|
||||
Param: (*modelkitDomain.ModelParam)(req.Parameters),
|
||||
})
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get model failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, model)
|
||||
}
|
||||
|
||||
// GetProviderSupportedModelList
|
||||
//
|
||||
// @Summary get provider supported model list
|
||||
// @Description get provider supported model list
|
||||
// @Tags model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params body domain.GetProviderModelListReq true "get supported model list request"
|
||||
// @Success 200 {object} domain.PWResponse{data=domain.GetProviderModelListResp}
|
||||
// @Router /api/v1/model/provider/supported [post]
|
||||
func (h *ModelHandler) GetProviderSupportedModelList(c echo.Context) error {
|
||||
var req domain.GetProviderModelListReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
|
||||
models, err := h.modelkit.ModelList(ctx, &modelkitDomain.ModelListReq{
|
||||
Provider: req.Provider,
|
||||
BaseURL: req.BaseURL,
|
||||
APIKey: req.APIKey,
|
||||
APIHeader: req.APIHeader,
|
||||
Type: string(req.Type),
|
||||
})
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get user model list failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, models)
|
||||
}
|
||||
|
||||
// SwitchMode
|
||||
//
|
||||
// @Summary switch mode
|
||||
// @Description switch model mode between manual and auto
|
||||
// @Tags model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.SwitchModeReq true "switch mode request"
|
||||
// @Success 200 {object} domain.Response{data=domain.SwitchModeResp}
|
||||
// @Router /api/v1/model/switch-mode [post]
|
||||
func (h *ModelHandler) SwitchMode(c echo.Context) error {
|
||||
var req domain.SwitchModeReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "bind request failed", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
|
||||
if err := h.usecase.SwitchMode(ctx, &req); err != nil {
|
||||
return h.NewResponseWithError(c, err.Error(), err)
|
||||
}
|
||||
|
||||
resp := &domain.SwitchModeResp{
|
||||
Message: "模式切换成功",
|
||||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
||||
// GetModelModeSetting
|
||||
//
|
||||
// @Summary get model mode setting
|
||||
// @Description get current model mode setting including mode, API key and chat model
|
||||
// @Tags model
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.Response{data=domain.ModelModeSetting}
|
||||
// @Router /api/v1/model/mode-setting [get]
|
||||
func (h *ModelHandler) GetModelModeSetting(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
setting, err := h.usecase.GetModelModeSetting(ctx)
|
||||
if err != nil {
|
||||
// 如果获取失败,返回默认值(手动模式)
|
||||
h.logger.Warn("failed to get model mode setting, return default", log.Error(err))
|
||||
defaultSetting := domain.ModelModeSetting{
|
||||
Mode: consts.ModelSettingModeManual,
|
||||
AutoModeAPIKey: "",
|
||||
ChatModel: "",
|
||||
}
|
||||
return h.NewResponseWithData(c, defaultSetting)
|
||||
}
|
||||
return h.NewResponseWithData(c, setting)
|
||||
}
|
||||
182
backend/handler/v1/nav.go
Normal file
182
backend/handler/v1/nav.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/nav/v1"
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"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 NavHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
usecase *usecase.NavUsecase
|
||||
auth middleware.AuthMiddleware
|
||||
}
|
||||
|
||||
func NewNavHandler(
|
||||
baseHandler *handler.BaseHandler,
|
||||
echo *echo.Echo,
|
||||
usecase *usecase.NavUsecase,
|
||||
auth middleware.AuthMiddleware,
|
||||
logger *log.Logger,
|
||||
) *NavHandler {
|
||||
h := &NavHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.v1.nav"),
|
||||
usecase: usecase,
|
||||
auth: auth,
|
||||
}
|
||||
|
||||
group := echo.Group("/api/v1/nav", h.auth.Authorize, h.auth.ValidateKBUserPerm(consts.UserKBPermissionDocManage))
|
||||
group.GET("/list", h.NavList)
|
||||
group.POST("/add", h.NavAdd)
|
||||
group.DELETE("/delete", h.NavDelete)
|
||||
group.PATCH("/update", h.NavUpdate)
|
||||
group.POST("/move", h.NavMove)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// NavList
|
||||
//
|
||||
// @Summary 获取分栏列表
|
||||
// @Description Get Nav List
|
||||
// @Tags Nav
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param params query v1.NavListReq true "Params"
|
||||
// @Success 200 {object} domain.PWResponse{data=[]v1.NavListResp}
|
||||
// @Router /api/v1/nav/list [get]
|
||||
func (h *NavHandler) NavList(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var req v1.NavListReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
nodes, err := h.usecase.GetList(ctx, req.KbId)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get nav list failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nodes)
|
||||
}
|
||||
|
||||
// NavAdd
|
||||
//
|
||||
// @Summary 添加分栏
|
||||
// @Description Add Nav
|
||||
// @Tags Nav
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param body body v1.NavAddReq true "Params"
|
||||
// @Success 200 {object} domain.PWResponse
|
||||
// @Router /api/v1/nav/add [post]
|
||||
func (h *NavHandler) NavAdd(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var req v1.NavAddReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.Add(ctx, &req); err != nil {
|
||||
return h.NewResponseWithError(c, "add nav failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
|
||||
}
|
||||
|
||||
// NavDelete
|
||||
//
|
||||
// @Summary 删除栏目
|
||||
// @Description DeleteNav Nav
|
||||
// @Tags Nav
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param query query v1.NavDeleteReq true "Params"
|
||||
// @Success 200 {object} domain.PWResponse
|
||||
// @Router /api/v1/nav/delete [delete]
|
||||
func (h *NavHandler) NavDelete(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var req v1.NavDeleteReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.Delete(ctx, &req); err != nil {
|
||||
return h.NewResponseWithError(c, "delete nav failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// NavMove
|
||||
//
|
||||
// @Summary 移动栏目
|
||||
// @Description Move Nav
|
||||
// @Tags Nav
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param body body v1.NavMoveReq true "Params"
|
||||
// @Success 200 {object} domain.PWResponse
|
||||
// @Router /api/v1/nav/move [post]
|
||||
func (h *NavHandler) NavMove(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
var req v1.NavMoveReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
if err := h.usecase.Move(ctx, &req); err != nil {
|
||||
return h.NewResponseWithError(c, "move nav failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// NavUpdate
|
||||
//
|
||||
// @Summary 更新栏目信息
|
||||
// @Description Update Nav
|
||||
// @Tags Nav
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param body body v1.NavUpdateReq true "Params"
|
||||
// @Success 200 {object} domain.PWResponse
|
||||
// @Router /api/v1/nav/update [patch]
|
||||
func (h *NavHandler) NavUpdate(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var req v1.NavUpdateReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.Update(ctx, &req); err != nil {
|
||||
return h.NewResponseWithError(c, "update nav failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
565
backend/handler/v1/node.go
Normal file
565
backend/handler/v1/node.go
Normal file
@@ -0,0 +1,565 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/node/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 NodeHandler struct {
|
||||
*handler.BaseHandler
|
||||
logger *log.Logger
|
||||
usecase *usecase.NodeUsecase
|
||||
auth middleware.AuthMiddleware
|
||||
}
|
||||
|
||||
func NewNodeHandler(
|
||||
baseHandler *handler.BaseHandler,
|
||||
echo *echo.Echo,
|
||||
usecase *usecase.NodeUsecase,
|
||||
auth middleware.AuthMiddleware,
|
||||
logger *log.Logger,
|
||||
) *NodeHandler {
|
||||
h := &NodeHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: logger.WithModule("handler.v1.node"),
|
||||
usecase: usecase,
|
||||
auth: auth,
|
||||
}
|
||||
|
||||
group := echo.Group("/api/v1/node", h.auth.Authorize, h.auth.ValidateKBUserPerm(consts.UserKBPermissionDocManage))
|
||||
group.GET("/list", h.GetNodeList)
|
||||
group.GET("/list/group/nav", h.NodeListGroupNav)
|
||||
group.GET("/stats", h.NodeStats)
|
||||
|
||||
group.POST("", h.CreateNode)
|
||||
group.GET("/detail", h.GetNodeDetail)
|
||||
group.PUT("/detail", h.UpdateNodeDetail)
|
||||
group.POST("/summary", h.SummaryNode)
|
||||
group.POST("/summary/stream", h.SummaryNodeStream)
|
||||
|
||||
group.POST("/action", h.NodeAction)
|
||||
group.POST("/move", h.MoveNode)
|
||||
group.POST("/move/nav", h.NodeMoveNav)
|
||||
group.POST("/batch_move", h.BatchMoveNode)
|
||||
|
||||
group.GET("/recommend_nodes", h.RecommendNodes)
|
||||
group.POST("/restudy", h.NodeRestudy)
|
||||
|
||||
// node permission
|
||||
group.GET("/permission", h.NodePermission)
|
||||
group.PATCH("/permission/edit", h.NodePermissionEdit)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// CreateNode
|
||||
//
|
||||
// @Summary Create Node
|
||||
// @Description Create Node
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param body body domain.CreateNodeReq true "Node"
|
||||
// @Success 200 {object} domain.PWResponse{data=map[string]string}
|
||||
// @Router /api/v1/node [post]
|
||||
func (h *NodeHandler) CreateNode(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
authInfo := domain.GetAuthInfoFromCtx(ctx)
|
||||
if authInfo == nil {
|
||||
return h.NewResponseWithError(c, "authInfo not found in context", nil)
|
||||
}
|
||||
|
||||
req := &domain.CreateNodeReq{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
|
||||
req.MaxNode = domain.GetBaseEditionLimitation(ctx).MaxNode
|
||||
|
||||
id, err := h.usecase.Create(c.Request().Context(), req, authInfo.UserId)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrMaxNodeLimitReached) {
|
||||
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到更高版本", nil)
|
||||
}
|
||||
return h.NewResponseWithError(c, "create node failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, map[string]any{
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
// NodeStats
|
||||
//
|
||||
// @Summary Get Node Statistics
|
||||
// @Description Get Node Statistics
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param kb_id query v1.NodeStatsReq true "Knowledge Base ID"
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.NodeStatsResp}
|
||||
// @Router /api/v1/node/stats [get]
|
||||
func (h *NodeHandler) NodeStats(c echo.Context) error {
|
||||
var req v1.NodeStatsReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
stats, err := h.usecase.GetNodeStats(ctx, req.KbId)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get node stats failed", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, stats)
|
||||
}
|
||||
|
||||
// GetNodeList
|
||||
//
|
||||
// @Summary Get Node List
|
||||
// @Description Get Node List
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param params query domain.GetNodeListReq true "Params"
|
||||
// @Success 200 {object} domain.PWResponse{data=[]domain.NodeListItemResp}
|
||||
// @Router /api/v1/node/list [get]
|
||||
func (h *NodeHandler) GetNodeList(c echo.Context) error {
|
||||
var req domain.GetNodeListReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
nodes, err := h.usecase.GetList(ctx, &req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get node list failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nodes)
|
||||
}
|
||||
|
||||
// NodeListGroupNav
|
||||
//
|
||||
// @Summary Get Node List Grouped by Nav
|
||||
// @Description Get unpublished or unstudied document list grouped by nav
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param params query v1.NodeListGroupNavReq true "Params"
|
||||
// @Success 200 {object} domain.PWResponse{data=[]v1.NodeListGroupNavResp}
|
||||
// @Router /api/v1/node/list/group/nav [get]
|
||||
func (h *NodeHandler) NodeListGroupNav(c echo.Context) error {
|
||||
var req v1.NodeListGroupNavReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
result, err := h.usecase.GetNodeListGroupByNav(ctx, req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get node list group by nav failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, result)
|
||||
}
|
||||
|
||||
// GetNodeDetail
|
||||
//
|
||||
// @Summary Get Node Detail
|
||||
// @Description Get Node Detail
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param param query v1.GetNodeDetailReq true "conversation id"
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.NodeDetailResp}
|
||||
// @Router /api/v1/node/detail [get]
|
||||
func (h *NodeHandler) GetNodeDetail(c echo.Context) error {
|
||||
|
||||
var req v1.GetNodeDetailReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
node, err := h.usecase.GetNodeByKBID(c.Request().Context(), req.ID, req.KbId, req.Format)
|
||||
if err != nil {
|
||||
h.logger.Error("get node by kb id failed", log.Error(err))
|
||||
return h.NewResponseWithError(c, "get node detail failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, node)
|
||||
}
|
||||
|
||||
// NodeAction
|
||||
//
|
||||
// @Summary Node Action
|
||||
// @Description Node Action
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param action body domain.NodeActionReq true "Action"
|
||||
// @Success 200 {object} domain.PWResponse{data=map[string]string}
|
||||
// @Router /api/v1/node/action [post]
|
||||
func (h *NodeHandler) NodeAction(c echo.Context) error {
|
||||
req := &domain.NodeActionReq{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
if err := h.usecase.NodeAction(ctx, req); err != nil {
|
||||
return h.NewResponseWithError(c, "node action failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// UpdateNodeDetail
|
||||
//
|
||||
// @Summary Update Node Detail
|
||||
// @Description Update Node Detail
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param body body domain.UpdateNodeReq true "Node"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/node/detail [put]
|
||||
func (h *NodeHandler) UpdateNodeDetail(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
authInfo := domain.GetAuthInfoFromCtx(ctx)
|
||||
if authInfo == nil {
|
||||
return h.NewResponseWithError(c, "authInfo not found in context", nil)
|
||||
}
|
||||
|
||||
req := &domain.UpdateNodeReq{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.Update(ctx, req, authInfo.UserId); err != nil {
|
||||
return h.NewResponseWithError(c, "update node detail failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// MoveNode
|
||||
//
|
||||
// @Summary Move Node
|
||||
// @Description Move Node
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param body body domain.MoveNodeReq true "Move Node"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/node/move [post]
|
||||
func (h *NodeHandler) MoveNode(c echo.Context) error {
|
||||
req := &domain.MoveNodeReq{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
if err := h.usecase.MoveNode(ctx, req); err != nil {
|
||||
return h.NewResponseWithError(c, "move node failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// NodeMoveNav
|
||||
//
|
||||
// @Summary Move Node to Nav
|
||||
// @Description Move node (and all its descendants if folder) to a different nav
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param body body v1.NodeMoveNavReq true "Move Node Nav"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/node/move/nav [post]
|
||||
func (h *NodeHandler) NodeMoveNav(c echo.Context) error {
|
||||
req := &v1.NodeMoveNavReq{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
if err := h.usecase.MoveNodeNav(ctx, req); err != nil {
|
||||
return h.NewResponseWithError(c, "move node nav failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// SummaryNode
|
||||
//
|
||||
// @Summary Summary Node 异步后台生成
|
||||
// @Description Summary Node
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param body body domain.NodeSummaryReq true "Summary Node"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/node/summary [post]
|
||||
func (h *NodeHandler) SummaryNode(c echo.Context) error {
|
||||
req := &domain.NodeSummaryReq{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
err := h.usecase.SummaryNode(ctx, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrModelNotConfigured) {
|
||||
return h.NewResponseWithError(c, "请前往管理后台,点击右上角的“系统设置”配置推理大模型。", err)
|
||||
}
|
||||
return h.NewResponseWithError(c, "summary node failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// SummaryNodeStream
|
||||
//
|
||||
// @Summary Stream Summary Node
|
||||
// @Description Stream Summary Node for single document
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce text/event-stream
|
||||
// @Security bearerAuth
|
||||
// @Param body body domain.NodeSummaryReq true "Summary Node"
|
||||
// @Success 200 {string} string "SSE stream"
|
||||
// @Router /api/v1/node/summary/stream [post]
|
||||
func (h *NodeHandler) SummaryNodeStream(c echo.Context) error {
|
||||
req := &domain.NodeSummaryReq{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
if len(req.IDs) != 1 {
|
||||
return h.NewResponseWithError(c, "stream summary only supports single node", nil)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
|
||||
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")
|
||||
|
||||
err := h.usecase.StreamSummaryNode(ctx, req, func(ctx context.Context, dataType, chunk string) error {
|
||||
return h.writeSSEEvent(c, domain.SSEEvent{Type: dataType, Content: chunk})
|
||||
})
|
||||
if err != nil {
|
||||
msg := "summary node failed"
|
||||
if errors.Is(err, domain.ErrModelNotConfigured) {
|
||||
msg = "请前往管理后台,点击右上角的“系统设置”配置推理大模型。"
|
||||
}
|
||||
if writeErr := h.writeSSEEvent(c, domain.SSEEvent{Type: "error", Content: msg, Error: err.Error()}); writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return h.writeSSEEvent(c, domain.SSEEvent{Type: "done"})
|
||||
}
|
||||
|
||||
func (h *NodeHandler) 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
|
||||
}
|
||||
|
||||
// RecommendNodes
|
||||
//
|
||||
// @Summary Recommend Nodes
|
||||
// @Description Recommend Nodes
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param query query domain.GetRecommendNodeListReq true "Recommend Nodes"
|
||||
// @Success 200 {object} domain.PWResponse{data=[]domain.RecommendNodeListResp}
|
||||
// @Router /api/v1/node/recommend_nodes [get]
|
||||
func (h *NodeHandler) RecommendNodes(c echo.Context) error {
|
||||
var req domain.GetRecommendNodeListReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request params is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
if len(req.NodeIDs) == 0 && len(req.NavIds) == 0 {
|
||||
return h.NewResponseWithError(c, "node_ids or nav_ids is required", nil)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
nodes, err := h.usecase.GetRecommendNodeList(ctx, &req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get recommend nodes failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nodes)
|
||||
}
|
||||
|
||||
// BatchMoveNode
|
||||
//
|
||||
// @Summary Batch Move Node
|
||||
// @Description Batch Move Node
|
||||
// @Tags node
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param body body domain.BatchMoveReq true "Batch Move Node"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/node/batch_move [post]
|
||||
func (h *NodeHandler) BatchMoveNode(c echo.Context) error {
|
||||
req := &domain.BatchMoveReq{}
|
||||
if err := c.Bind(req); err != nil {
|
||||
return h.NewResponseWithError(c, "request body is invalid", err)
|
||||
}
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
if err := h.usecase.BatchMoveNode(ctx, req); err != nil {
|
||||
return h.NewResponseWithError(c, "batch move node failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// NodePermission 文档授权信息获取
|
||||
//
|
||||
// @Tags NodePermission
|
||||
// @Summary 文档授权信息获取
|
||||
// @Description 文档授权信息获取
|
||||
// @ID v1-NodePermission
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param param query v1.NodePermissionReq true "para"
|
||||
// @Success 200 {object} domain.Response{data=v1.NodePermissionResp}
|
||||
// @Router /api/v1/node/permission [get]
|
||||
func (h *NodeHandler) NodePermission(c echo.Context) error {
|
||||
var req v1.NodePermissionReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request params is invalid", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
release, err := h.usecase.GetNodePermissionsByID(ctx, req.ID, req.KbId)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get node permission detail failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, release)
|
||||
}
|
||||
|
||||
// NodePermissionEdit 文档授权信息更新
|
||||
//
|
||||
// @Tags NodePermission
|
||||
// @Summary 文档授权信息更新
|
||||
// @Description 文档授权信息更新
|
||||
// @ID v1-NodePermissionEdit
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param param body v1.NodePermissionEditReq true "para"
|
||||
// @Success 200 {object} domain.Response{data=v1.NodePermissionEditResp}
|
||||
// @Router /api/v1/node/permission/edit [patch]
|
||||
func (h *NodeHandler) NodePermissionEdit(c echo.Context) error {
|
||||
var req v1.NodePermissionEditReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request params is invalid", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.ValidateNodePermissionsEdit(req, consts.GetLicenseEdition(c)); err != nil {
|
||||
return h.NewResponseWithError(c, "validate node permission failed", err)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
err := h.usecase.NodePermissionsEdit(ctx, req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "update node permission failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// NodeRestudy 文档重新学习
|
||||
//
|
||||
// @Tags Node
|
||||
// @Summary 文档重新学习
|
||||
// @Description 文档重新学习
|
||||
// @ID v1-NodeRestudy
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param param body v1.NodeRestudyReq true "para"
|
||||
// @Success 200 {object} domain.Response{data=v1.NodeRestudyResp}
|
||||
// @Router /api/v1/node/restudy [post]
|
||||
func (h *NodeHandler) NodeRestudy(c echo.Context) error {
|
||||
var req v1.NodeRestudyReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "request params is invalid", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request params failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.NodeRestudy(c.Request().Context(), &req); err != nil {
|
||||
return h.NewResponseWithError(c, err.Error(), err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
47
backend/handler/v1/provider.go
Normal file
47
backend/handler/v1/provider.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/middleware"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type APIHandlers struct {
|
||||
UserHandler *UserHandler
|
||||
KnowledgeBaseHandler *KnowledgeBaseHandler
|
||||
NodeHandler *NodeHandler
|
||||
AppHandler *AppHandler
|
||||
FileHandler *FileHandler
|
||||
ModelHandler *ModelHandler
|
||||
ConversationHandler *ConversationHandler
|
||||
CrawlerHandler *CrawlerHandler
|
||||
CreationHandler *CreationHandler
|
||||
StatHandler *StatHandler
|
||||
CommentHandler *CommentHandler
|
||||
AuthV1Handler *AuthV1Handler
|
||||
NavHandler *NavHandler
|
||||
}
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
middleware.ProviderSet,
|
||||
usecase.ProviderSet,
|
||||
|
||||
handler.NewBaseHandler,
|
||||
NewNodeHandler,
|
||||
NewAppHandler,
|
||||
NewConversationHandler,
|
||||
NewUserHandler,
|
||||
NewFileHandler,
|
||||
NewModelHandler,
|
||||
NewKnowledgeBaseHandler,
|
||||
NewCrawlerHandler,
|
||||
NewCreationHandler,
|
||||
NewStatHandler,
|
||||
NewCommentHandler,
|
||||
NewAuthV1Handler,
|
||||
NewNavHandler,
|
||||
|
||||
wire.Struct(new(APIHandlers), "*"),
|
||||
)
|
||||
295
backend/handler/v1/stat.go
Normal file
295
backend/handler/v1/stat.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/stat/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 StatHandler struct {
|
||||
*handler.BaseHandler
|
||||
usecase *usecase.StatUseCase
|
||||
auth middleware.AuthMiddleware
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewStatHandler(baseHandler *handler.BaseHandler, echo *echo.Echo, usecase *usecase.StatUseCase, logger *log.Logger, auth middleware.AuthMiddleware) *StatHandler {
|
||||
h := &StatHandler{
|
||||
BaseHandler: baseHandler,
|
||||
usecase: usecase,
|
||||
auth: auth,
|
||||
logger: logger.WithModule("handler.v1.stat"),
|
||||
}
|
||||
|
||||
group := echo.Group("/api/v1/stat", h.auth.Authorize, auth.ValidateKBUserPerm(consts.UserKBPermissionDataOperate))
|
||||
|
||||
// 实时
|
||||
group.GET("/instant_count", h.GetInstantCount) // instant count (30min, every 1min)
|
||||
group.GET("/instant_pages", h.GetInstantPages) // instant pages (latest 10 pages)
|
||||
|
||||
// 周期统计
|
||||
group.GET("/count", h.StatCount)
|
||||
group.GET("/geo_count", h.StatGeoCountReq) // geo (24h)
|
||||
group.GET("/conversation_distribution", h.StatConversationDistribution) // conversation (24h)
|
||||
group.GET("/hot_pages", h.StatHotPages)
|
||||
group.GET("/referer_hosts", h.StatRefererHosts)
|
||||
group.GET("/browsers", h.StatBrowsers)
|
||||
return h
|
||||
}
|
||||
|
||||
// StatCount 全局统计
|
||||
//
|
||||
// @Summary 全局统计
|
||||
// @Description 全局统计
|
||||
// @Tags stat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param para query v1.StatCountReq true "para"
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.StatCountResp}
|
||||
// @Router /api/v1/stat/count [get]
|
||||
func (h *StatHandler) StatCount(c echo.Context) error {
|
||||
var req v1.StatCountReq
|
||||
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, "validation failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.ValidateStatDay(req.Day, consts.GetLicenseEdition(c)); err != nil {
|
||||
h.logger.Error("validate stat day failed")
|
||||
return h.NewResponseWithErrCode(c, domain.ErrCodePermissionDenied)
|
||||
}
|
||||
|
||||
count, err := h.usecase.GetStatCount(c.Request().Context(), req.KbID, req.Day)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get count failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, count)
|
||||
}
|
||||
|
||||
// StatGeoCountReq 用户地理分布
|
||||
//
|
||||
// @Summary 用户地理分布
|
||||
// @Description 用户地理分布
|
||||
// @Tags stat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param para query v1.StatGeoCountReq true "para"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/stat/geo_count [get]
|
||||
func (h *StatHandler) StatGeoCountReq(c echo.Context) error {
|
||||
var req v1.StatGeoCountReq
|
||||
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, "validation failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.ValidateStatDay(req.Day, consts.GetLicenseEdition(c)); err != nil {
|
||||
h.logger.Error("validate stat day failed")
|
||||
return h.NewResponseWithErrCode(c, domain.ErrCodePermissionDenied)
|
||||
}
|
||||
|
||||
geoCount, err := h.usecase.GetGeoCount(c.Request().Context(), req.KbID, req.Day)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get geo count failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, geoCount)
|
||||
}
|
||||
|
||||
// StatConversationDistribution
|
||||
//
|
||||
// @Summary 问答来源
|
||||
// @Description 问答来源
|
||||
// @Tags stat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param para query v1.StatConversationDistributionReq true "para"
|
||||
// @Success 200 {object} domain.Response{data=[]v1.StatConversationDistributionResp}
|
||||
// @Router /api/v1/stat/conversation_distribution [get]
|
||||
func (h *StatHandler) StatConversationDistribution(c echo.Context) error {
|
||||
var req v1.StatConversationDistributionReq
|
||||
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, "validation failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.ValidateStatDay(req.Day, consts.GetLicenseEdition(c)); err != nil {
|
||||
h.logger.Error("validate stat day failed")
|
||||
return h.NewResponseWithErrCode(c, domain.ErrCodePermissionDenied)
|
||||
}
|
||||
|
||||
distribution, err := h.usecase.GetConversationDistribution(c.Request().Context(), req.KbID, req.Day)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get conversation distribution failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, distribution)
|
||||
}
|
||||
|
||||
// StatHotPages 热门文档
|
||||
//
|
||||
// @Summary 热门文档
|
||||
// @Description 热门文档
|
||||
// @Tags stat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param para query v1.StatHotPagesReq true "para"
|
||||
// @Success 200 {object} domain.Response{data=[]domain.HotPage}
|
||||
// @Router /api/v1/stat/hot_pages [get]
|
||||
func (h *StatHandler) StatHotPages(c echo.Context) error {
|
||||
var req v1.StatHotPagesReq
|
||||
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, "validation failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.ValidateStatDay(req.Day, consts.GetLicenseEdition(c)); err != nil {
|
||||
return h.NewResponseWithError(c, err.Error(), err)
|
||||
}
|
||||
|
||||
hotPages, err := h.usecase.GetHotPages(c.Request().Context(), req.KbID, req.Day)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get hot pages failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, hotPages)
|
||||
}
|
||||
|
||||
// StatRefererHosts 来源域名
|
||||
//
|
||||
// @Summary 来源域名
|
||||
// @Description 来源域名
|
||||
// @Tags stat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param para query v1.StatRefererHostsReq true "para"
|
||||
// @Success 200 {object} domain.Response{data=[]domain.HotRefererHost}
|
||||
// @Router /api/v1/stat/referer_hosts [get]
|
||||
func (h *StatHandler) StatRefererHosts(c echo.Context) error {
|
||||
var req v1.StatRefererHostsReq
|
||||
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, "validation failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.ValidateStatDay(req.Day, consts.GetLicenseEdition(c)); err != nil {
|
||||
return h.NewResponseWithError(c, err.Error(), err)
|
||||
}
|
||||
|
||||
refererHosts, err := h.usecase.GetHotRefererHosts(c.Request().Context(), req.KbID, req.Day)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get hot referer hosts failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, refererHosts)
|
||||
}
|
||||
|
||||
// StatBrowsers 客户端统计
|
||||
//
|
||||
// @Summary 客户端统计
|
||||
// @Description 客户端统计
|
||||
// @Tags stat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param para query v1.StatBrowsersReq true "para"
|
||||
// @Success 200 {object} domain.Response{data=domain.HotBrowser}
|
||||
// @Router /api/v1/stat/browsers [get]
|
||||
func (h *StatHandler) StatBrowsers(c echo.Context) error {
|
||||
var req v1.StatBrowsersReq
|
||||
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, "validation failed", err)
|
||||
}
|
||||
|
||||
if err := h.usecase.ValidateStatDay(req.Day, consts.GetLicenseEdition(c)); err != nil {
|
||||
return h.NewResponseWithError(c, err.Error(), err)
|
||||
}
|
||||
|
||||
hotBrowsers, err := h.usecase.GetHotBrowsers(c.Request().Context(), req.KbID, req.Day)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get hot browsers failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, hotBrowsers)
|
||||
}
|
||||
|
||||
// GetInstantCount get instant count
|
||||
//
|
||||
// @Summary GetInstantCount
|
||||
// @Description GetInstantCount
|
||||
// @Tags stat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param para query v1.StatInstantCountReq true "para"
|
||||
// @Success 200 {object} domain.Response{data=[]domain.InstantCountResp}
|
||||
// @Router /api/v1/stat/instant_count [get]
|
||||
func (h *StatHandler) GetInstantCount(c echo.Context) error {
|
||||
var req v1.StatInstantCountReq
|
||||
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, "validation failed", err)
|
||||
}
|
||||
|
||||
count, err := h.usecase.GetInstantCount(c.Request().Context(), req.KbID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get instant count failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, count)
|
||||
}
|
||||
|
||||
// GetInstantPages get instant pages
|
||||
//
|
||||
// @Summary GetInstantPages
|
||||
// @Description GetInstantPages
|
||||
// @Tags stat
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security bearerAuth
|
||||
// @Param para query v1.StatInstantPagesReq true "para"
|
||||
// @Success 200 {object} domain.Response{data=[]domain.InstantPageResp}
|
||||
// @Router /api/v1/stat/instant_pages [get]
|
||||
func (h *StatHandler) GetInstantPages(c echo.Context) error {
|
||||
var req v1.StatInstantPagesReq
|
||||
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, "validation failed", err)
|
||||
}
|
||||
|
||||
pages, err := h.usecase.GetInstantPages(c.Request().Context(), req.KbID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "get instant pages failed", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, pages)
|
||||
}
|
||||
302
backend/handler/v1/user.go
Normal file
302
backend/handler/v1/user.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
v1 "github.com/chaitin/panda-wiki/api/user/v1"
|
||||
"github.com/chaitin/panda-wiki/config"
|
||||
"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/pkg/ratelimit"
|
||||
"github.com/chaitin/panda-wiki/store/cache"
|
||||
"github.com/chaitin/panda-wiki/usecase"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
*handler.BaseHandler
|
||||
usecase *usecase.UserUsecase
|
||||
logger *log.Logger
|
||||
config *config.Config
|
||||
auth middleware.AuthMiddleware
|
||||
rateLimiter *ratelimit.RateLimiter
|
||||
}
|
||||
|
||||
func NewUserHandler(e *echo.Echo, baseHandler *handler.BaseHandler, logger *log.Logger, usecase *usecase.UserUsecase, auth middleware.AuthMiddleware, config *config.Config, cache *cache.Cache) *UserHandler {
|
||||
handlerLogger := logger.WithModule("handler.v1.user")
|
||||
h := &UserHandler{
|
||||
BaseHandler: baseHandler,
|
||||
logger: handlerLogger,
|
||||
usecase: usecase,
|
||||
auth: auth,
|
||||
config: config,
|
||||
rateLimiter: ratelimit.NewRateLimiter(handlerLogger, cache),
|
||||
}
|
||||
group := e.Group("/api/v1/user")
|
||||
group.POST("/login", h.Login)
|
||||
|
||||
group.GET("", h.GetUserInfo, h.auth.Authorize)
|
||||
group.GET("/list", h.ListUsers, h.auth.Authorize)
|
||||
group.POST("/create", h.CreateUser, h.auth.Authorize, h.auth.ValidateUserRole(consts.UserRoleAdmin))
|
||||
group.PUT("/reset_password", h.ResetPassword, h.auth.Authorize, h.auth.ValidateUserRole(consts.UserRoleAdmin))
|
||||
group.DELETE("/delete", h.DeleteUser, h.auth.Authorize, h.auth.ValidateUserRole(consts.UserRoleAdmin))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// CreateUser
|
||||
//
|
||||
// @Summary CreateUser
|
||||
// @Description CreateUser
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body v1.CreateUserReq true "CreateUser Request"
|
||||
// @Success 200 {object} domain.Response{data=v1.CreateUserResp}
|
||||
// @Router /api/v1/user/create [post]
|
||||
func (h *UserHandler) CreateUser(c echo.Context) error {
|
||||
var req v1.CreateUserReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
uid := uuid.New().String()
|
||||
err := h.usecase.CreateUser(c.Request().Context(), &domain.User{
|
||||
ID: uid,
|
||||
Account: req.Account,
|
||||
Password: req.Password,
|
||||
Role: req.Role,
|
||||
}, consts.GetLicenseEdition(c))
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to create user", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, v1.CreateUserResp{ID: uid})
|
||||
}
|
||||
|
||||
// Login
|
||||
//
|
||||
// @Summary Login
|
||||
// @Description Login
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body v1.LoginReq true "Login Request"
|
||||
// @Success 200 {object} v1.LoginResp
|
||||
// @Router /api/v1/user/login [post]
|
||||
func (h *UserHandler) Login(c echo.Context) error {
|
||||
var req v1.LoginReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
ip := c.RealIP()
|
||||
locked, remaining := h.rateLimiter.CheckIPLocked(ctx, ip)
|
||||
if locked {
|
||||
h.logger.Warn("IP is locked", "ip", ip, "remaining", remaining)
|
||||
return h.NewResponseWithError(c, fmt.Sprintf("账号已被锁定,请 %s 后重试", remaining.String()), nil)
|
||||
}
|
||||
|
||||
token, err := h.usecase.VerifyUserAndGenerateToken(ctx, req)
|
||||
if err != nil {
|
||||
h.rateLimiter.LockAttempt(ctx, ip)
|
||||
return h.NewResponseWithError(c, "用户名或密码错误", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := h.rateLimiter.ResetLoginAttempts(context.Background(), ip); err != nil {
|
||||
h.logger.Error("failed to reset login attempts", "error", err, "ip", ip)
|
||||
}
|
||||
}()
|
||||
|
||||
return h.NewResponseWithData(c, v1.LoginResp{Token: token})
|
||||
}
|
||||
|
||||
// GetUserInfo
|
||||
//
|
||||
// @Summary GetUser
|
||||
// @Description GetUser
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Success 200 {object} v1.UserInfoResp
|
||||
// @Router /api/v1/user [get]
|
||||
func (h *UserHandler) GetUserInfo(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
authInfo := domain.GetAuthInfoFromCtx(ctx)
|
||||
if authInfo == nil {
|
||||
return h.NewResponseWithError(c, "authInfo not found in context", nil)
|
||||
}
|
||||
|
||||
user, err := h.usecase.GetUser(c.Request().Context(), authInfo.UserId)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get user", err)
|
||||
}
|
||||
|
||||
userInfo := &v1.UserInfoResp{
|
||||
ID: user.ID,
|
||||
Account: user.Account,
|
||||
Role: user.Role,
|
||||
IsToken: authInfo.IsToken,
|
||||
LastAccess: &user.LastAccess,
|
||||
CreatedAt: user.CreatedAt,
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, userInfo)
|
||||
}
|
||||
|
||||
// ListUsers
|
||||
//
|
||||
// @Summary ListUsers
|
||||
// @Description ListUsers
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} domain.PWResponse{data=v1.UserListResp}
|
||||
// @Router /api/v1/user/list [get]
|
||||
func (h *UserHandler) ListUsers(c echo.Context) error {
|
||||
var req v1.UserListReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
users, err := h.usecase.ListUsers(c.Request().Context())
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to list users", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, users)
|
||||
}
|
||||
|
||||
// ResetPassword
|
||||
//
|
||||
// @Summary ResetPassword
|
||||
// @Description ResetPassword
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body v1.ResetPasswordReq true "ResetPassword Request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/user/reset_password [put]
|
||||
func (h *UserHandler) ResetPassword(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
var req v1.ResetPasswordReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
authInfo := domain.GetAuthInfoFromCtx(ctx)
|
||||
if authInfo == nil {
|
||||
return h.NewResponseWithError(c, "authInfo not found in context", nil)
|
||||
}
|
||||
|
||||
if authInfo.IsToken {
|
||||
return h.NewResponseWithError(c, "this api not support token call", nil)
|
||||
}
|
||||
|
||||
user, err := h.usecase.GetUser(ctx, authInfo.UserId)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get user", err)
|
||||
}
|
||||
|
||||
if user.Account == "admin" {
|
||||
// admin 改不了自己的密码
|
||||
if authInfo.UserId == req.ID {
|
||||
return h.NewResponseWithError(c, "请修改安装目录下 .env 文件中的 ADMIN_PASSWORD,并重启 panda-wiki-api 容器使更改生效。", nil)
|
||||
}
|
||||
} else {
|
||||
targetUser, err := h.usecase.GetUser(ctx, req.ID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get target user", err)
|
||||
}
|
||||
|
||||
// 超级管理员不能改其他超级管理员密码
|
||||
if targetUser.Role == consts.UserRoleAdmin && targetUser.ID != authInfo.UserId {
|
||||
return h.NewResponseWithError(c, "无法修改其他超级管理员密码", nil)
|
||||
}
|
||||
}
|
||||
|
||||
err = h.usecase.ResetPassword(c.Request().Context(), &req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to reset password", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// DeleteUser
|
||||
//
|
||||
// @Summary DeleteUser
|
||||
// @Description DeleteUser
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param params query v1.DeleteUserReq true "DeleteUser Request"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/user/delete [delete]
|
||||
func (h *UserHandler) DeleteUser(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
var req v1.DeleteUserReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
authInfo := domain.GetAuthInfoFromCtx(ctx)
|
||||
if authInfo == nil {
|
||||
return h.NewResponseWithError(c, "authInfo not found in context", nil)
|
||||
}
|
||||
|
||||
if authInfo.IsToken {
|
||||
return h.NewResponseWithError(c, "this api not support token call", nil)
|
||||
}
|
||||
|
||||
if authInfo.UserId == req.UserID {
|
||||
return h.NewResponseWithError(c, "cannot delete yourself", nil)
|
||||
}
|
||||
|
||||
user, err := h.usecase.GetUser(ctx, authInfo.UserId)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get user", err)
|
||||
}
|
||||
|
||||
targetUser, err := h.usecase.GetUser(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get target user", err)
|
||||
}
|
||||
|
||||
if targetUser.Account == "admin" {
|
||||
return h.NewResponseWithError(c, "cannot delete admin user", nil)
|
||||
}
|
||||
|
||||
// 非admin账号的管理员只能删除普通用户的账户
|
||||
if user.Account != "admin" {
|
||||
if targetUser.Role != consts.UserRoleUser {
|
||||
return h.NewResponseWithError(c, "cannot delete other admin users", nil)
|
||||
}
|
||||
}
|
||||
|
||||
err = h.usecase.DeleteUser(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to delete user", err)
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
Reference in New Issue
Block a user