mirror of https://github.com/chaitin/PandaWiki.git
Compare commits
8 Commits
f26350573d
...
a65026472c
| Author | SHA1 | Date |
|---|---|---|
|
|
a65026472c | |
|
|
63a4a964b1 | |
|
|
62f2b2eaf5 | |
|
|
5c1c6368b8 | |
|
|
7282503acf | |
|
|
60a4177229 | |
|
|
2f706a6100 | |
|
|
febcb06654 |
|
|
@ -1,8 +1,6 @@
|
|||
package consts
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
|
|
@ -13,28 +11,13 @@ const ContextKeyEdition contextKey = "edition"
|
|||
type LicenseEdition int32
|
||||
|
||||
const (
|
||||
LicenseEditionFree LicenseEdition = 0 // 开源版
|
||||
LicenseEditionContributor LicenseEdition = 1 // 联创版
|
||||
LicenseEditionEnterprise LicenseEdition = 2 // 企业版
|
||||
LicenseEditionFree LicenseEdition = 0 // 开源版
|
||||
LicenseEditionProfession LicenseEdition = 1 // 专业版
|
||||
LicenseEditionEnterprise LicenseEdition = 2 // 企业版
|
||||
LicenseEditionBusiness LicenseEdition = 3 // 商业版
|
||||
)
|
||||
|
||||
func GetLicenseEdition(c echo.Context) LicenseEdition {
|
||||
edition, _ := c.Get("edition").(LicenseEdition)
|
||||
return edition
|
||||
}
|
||||
|
||||
func (e LicenseEdition) GetMaxAuth(sourceType SourceType) int {
|
||||
switch e {
|
||||
case LicenseEditionFree:
|
||||
if sourceType == SourceTypeGitHub {
|
||||
return 10
|
||||
}
|
||||
return 0
|
||||
case LicenseEditionContributor:
|
||||
return 10
|
||||
case LicenseEditionEnterprise:
|
||||
return math.MaxInt
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4067,22 +4067,26 @@ const docTemplate = `{
|
|||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
2,
|
||||
3
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"LicenseEditionContributor": "联创版",
|
||||
"LicenseEditionBusiness": "商业版",
|
||||
"LicenseEditionEnterprise": "企业版",
|
||||
"LicenseEditionFree": "开源版"
|
||||
"LicenseEditionFree": "开源版",
|
||||
"LicenseEditionProfession": "专业版"
|
||||
},
|
||||
"x-enum-descriptions": [
|
||||
"开源版",
|
||||
"联创版",
|
||||
"企业版"
|
||||
"专业版",
|
||||
"企业版",
|
||||
"商业版"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"LicenseEditionFree",
|
||||
"LicenseEditionContributor",
|
||||
"LicenseEditionEnterprise"
|
||||
"LicenseEditionProfession",
|
||||
"LicenseEditionEnterprise",
|
||||
"LicenseEditionBusiness"
|
||||
]
|
||||
},
|
||||
"consts.ModelSettingMode": {
|
||||
|
|
|
|||
|
|
@ -4060,22 +4060,26 @@
|
|||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
2,
|
||||
3
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"LicenseEditionContributor": "联创版",
|
||||
"LicenseEditionBusiness": "商业版",
|
||||
"LicenseEditionEnterprise": "企业版",
|
||||
"LicenseEditionFree": "开源版"
|
||||
"LicenseEditionFree": "开源版",
|
||||
"LicenseEditionProfession": "专业版"
|
||||
},
|
||||
"x-enum-descriptions": [
|
||||
"开源版",
|
||||
"联创版",
|
||||
"企业版"
|
||||
"专业版",
|
||||
"企业版",
|
||||
"商业版"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"LicenseEditionFree",
|
||||
"LicenseEditionContributor",
|
||||
"LicenseEditionEnterprise"
|
||||
"LicenseEditionProfession",
|
||||
"LicenseEditionEnterprise",
|
||||
"LicenseEditionBusiness"
|
||||
]
|
||||
},
|
||||
"consts.ModelSettingMode": {
|
||||
|
|
|
|||
|
|
@ -117,20 +117,24 @@ definitions:
|
|||
- 0
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
format: int32
|
||||
type: integer
|
||||
x-enum-comments:
|
||||
LicenseEditionContributor: 联创版
|
||||
LicenseEditionBusiness: 商业版
|
||||
LicenseEditionEnterprise: 企业版
|
||||
LicenseEditionFree: 开源版
|
||||
LicenseEditionProfession: 专业版
|
||||
x-enum-descriptions:
|
||||
- 开源版
|
||||
- 联创版
|
||||
- 专业版
|
||||
- 企业版
|
||||
- 商业版
|
||||
x-enum-varnames:
|
||||
- LicenseEditionFree
|
||||
- LicenseEditionContributor
|
||||
- LicenseEditionProfession
|
||||
- LicenseEditionEnterprise
|
||||
- LicenseEditionBusiness
|
||||
consts.ModelSettingMode:
|
||||
enum:
|
||||
- manual
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const ContextKeyEditionLimitation contextKey = "edition_limitation"
|
||||
|
||||
type BaseEditionLimitation struct {
|
||||
MaxKb int `json:"max_kb"` // 知识库站点数量
|
||||
MaxNode int `json:"max_node"` // 单个知识库下文档数量
|
||||
MaxSSOUser int `json:"max_sso_users"` // SSO认证用户数量
|
||||
MaxAdmin int64 `json:"max_admin"` // 后台管理员数量
|
||||
AllowAdminPerm bool `json:"allow_admin_perm"` // 支持管理员分权控制
|
||||
AllowCustomCopyright bool `json:"allow_custom_copyright"` // 支持自定义版权信息
|
||||
AllowCommentAudit bool `json:"allow_comment_audit"` // 支持评论审核
|
||||
AllowAdvancedBot bool `json:"allow_advanced_bot"` // 支持高级机器人配置
|
||||
AllowWatermark bool `json:"allow_watermark"` // 支持水印
|
||||
AllowCopyProtection bool `json:"allow_copy_protection"` // 支持内容复制保护
|
||||
AllowOpenAIBotSettings bool `json:"allow_open_ai_bot_settings"` // 支持问答机器人
|
||||
}
|
||||
|
||||
var baseEditionLimitationDefault = BaseEditionLimitation{
|
||||
MaxKb: 1,
|
||||
MaxAdmin: 1,
|
||||
MaxNode: 300,
|
||||
}
|
||||
|
||||
func GetBaseEditionLimitation(c context.Context) BaseEditionLimitation {
|
||||
|
||||
edition, ok := c.Value(ContextKeyEditionLimitation).([]byte)
|
||||
if !ok {
|
||||
return baseEditionLimitationDefault
|
||||
}
|
||||
|
||||
var editionLimitation BaseEditionLimitation
|
||||
if err := json.Unmarshal(edition, &editionLimitation); err != nil {
|
||||
return baseEditionLimitationDefault
|
||||
}
|
||||
|
||||
return editionLimitation
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ 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"
|
||||
|
|
@ -157,7 +156,7 @@ func (h *ShareCommentHandler) GetCommentList(c echo.Context) error {
|
|||
}
|
||||
|
||||
// 查询数据库获取所有评论-->0 所有, 1,2 为需要审核的评论
|
||||
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID, consts.GetLicenseEdition(c))
|
||||
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get comment list", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
v1 "github.com/chaitin/panda-wiki/api/kb/v1"
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
)
|
||||
|
||||
// KBUserList
|
||||
|
|
@ -55,8 +56,8 @@ func (h *KnowledgeBaseHandler) KBUserInvite(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
|
||||
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
|
||||
}
|
||||
|
||||
err := h.usecase.KBUserInvite(c.Request().Context(), req)
|
||||
|
|
@ -87,8 +88,8 @@ func (h *KnowledgeBaseHandler) KBUserUpdate(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
|
||||
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
|
||||
}
|
||||
|
||||
err := h.usecase.UpdateUserKB(c.Request().Context(), req)
|
||||
|
|
|
|||
|
|
@ -91,11 +91,7 @@ func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "ports is required", nil)
|
||||
}
|
||||
|
||||
req.MaxKB = 1
|
||||
maxKB := c.Get("max_kb")
|
||||
if maxKB != nil {
|
||||
req.MaxKB = maxKB.(int)
|
||||
}
|
||||
req.MaxKB = domain.GetBaseEditionLimitation(c.Request().Context()).MaxKb
|
||||
|
||||
did, err := h.usecase.CreateKnowledgeBase(c.Request().Context(), &req)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func NewModelHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *
|
|||
return handler
|
||||
}
|
||||
|
||||
// get model list
|
||||
// GetModelList
|
||||
//
|
||||
// @Summary get model list
|
||||
// @Description get model list
|
||||
|
|
@ -66,7 +66,7 @@ func (h *ModelHandler) GetModelList(c echo.Context) error {
|
|||
return h.NewResponseWithData(c, models)
|
||||
}
|
||||
|
||||
// create model
|
||||
// CreateModel
|
||||
//
|
||||
// @Summary create model
|
||||
// @Description create model
|
||||
|
|
@ -85,9 +85,6 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
|
||||
return h.NewResponseWithError(c, "联创版只能使用百智云模型哦~", nil)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
|
||||
param := domain.ModelParam{}
|
||||
|
|
@ -112,7 +109,7 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
|
|||
return h.NewResponseWithData(c, model)
|
||||
}
|
||||
|
||||
// update model
|
||||
// UpdateModel
|
||||
//
|
||||
// @Description update model
|
||||
// @Tags model
|
||||
|
|
@ -130,9 +127,6 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
|
||||
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)
|
||||
|
|
@ -140,7 +134,7 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
|
|||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// check model
|
||||
// CheckModel
|
||||
//
|
||||
// @Summary check model
|
||||
// @Description check model
|
||||
|
|
|
|||
|
|
@ -81,15 +81,13 @@ func (h *NodeHandler) CreateNode(c echo.Context) error {
|
|||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
req.MaxNode = 300
|
||||
if maxNode := c.Get("max_node"); maxNode != nil {
|
||||
req.MaxNode = maxNode.(int)
|
||||
}
|
||||
|
||||
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, "已达到最大文档数量限制,请升级到更高版本", nil)
|
||||
}
|
||||
return h.NewResponseWithError(c, "create node failed", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ type AuthMiddleware interface {
|
|||
Authorize(next echo.HandlerFunc) echo.HandlerFunc
|
||||
ValidateUserRole(role consts.UserRole) echo.MiddlewareFunc
|
||||
ValidateKBUserPerm(role consts.UserKBPermission) echo.MiddlewareFunc
|
||||
ValidateLicenseEdition(edition consts.LicenseEdition) echo.MiddlewareFunc
|
||||
ValidateLicenseEdition(edition ...consts.LicenseEdition) echo.MiddlewareFunc
|
||||
MustGetUserID(c echo.Context) (string, bool)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
|
@ -194,7 +195,7 @@ func (m *JWTMiddleware) ValidateKBUserPerm(perm consts.UserKBPermission) echo.Mi
|
|||
}
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) ValidateLicenseEdition(needEdition consts.LicenseEdition) echo.MiddlewareFunc {
|
||||
func (m *JWTMiddleware) ValidateLicenseEdition(needEditions ...consts.LicenseEdition) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
|
||||
|
|
@ -206,7 +207,7 @@ func (m *JWTMiddleware) ValidateLicenseEdition(needEdition consts.LicenseEdition
|
|||
})
|
||||
}
|
||||
|
||||
if edition < needEdition {
|
||||
if !slices.Contains(needEditions, edition) {
|
||||
return c.JSON(http.StatusForbidden, domain.PWResponse{
|
||||
Success: false,
|
||||
Message: "Unauthorized ValidateLicenseEdition",
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit c4dc498df094cb617d31c95580db8239a445d652
|
||||
Subproject commit bcf7e0f0bedb18f43cf36463ddb45ace6c1dbab9
|
||||
|
|
@ -300,8 +300,8 @@ func (r *AuthRepo) GetOrCreateAuth(ctx context.Context, auth *domain.Auth, sourc
|
|||
return err
|
||||
}
|
||||
|
||||
if int(count) >= licenseEdition.GetMaxAuth(sourceType) {
|
||||
return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, licenseEdition.GetMaxAuth(sourceType))
|
||||
if int(count) >= domain.GetBaseEditionLimitation(ctx).MaxSSOUser {
|
||||
return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, domain.GetBaseEditionLimitation(ctx).MaxSSOUser)
|
||||
}
|
||||
|
||||
auth.LastLoginTime = time.Now()
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ func (r *CommentRepository) CreateComment(ctx context.Context, comment *domain.C
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string, edition consts.LicenseEdition) ([]*domain.ShareCommentListItem, int64, error) {
|
||||
func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string) ([]*domain.ShareCommentListItem, int64, error) {
|
||||
// 按照时间排序来查询node_id的comments
|
||||
var comments []*domain.ShareCommentListItem
|
||||
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("node_id = ?", nodeID)
|
||||
|
||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
|
||||
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit {
|
||||
query = query.Where("status = ?", domain.CommentStatusAccepted) //accepted
|
||||
}
|
||||
|
||||
|
|
@ -50,14 +50,14 @@ func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string, e
|
|||
|
||||
func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domain.CommentListReq, edition consts.LicenseEdition) ([]*domain.CommentListItem, int64, error) {
|
||||
comments := []*domain.CommentListItem{}
|
||||
query := r.db.Model(&domain.Comment{}).Where("comments.kb_id = ?", req.KbID)
|
||||
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("comments.kb_id = ?", req.KbID)
|
||||
var count int64
|
||||
if req.Status == nil {
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
} else {
|
||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
|
||||
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit {
|
||||
query = query.Where("comments.status = ?", *req.Status)
|
||||
}
|
||||
// 按照时间排序来查询kb_id的comments ->reject pending accepted
|
||||
|
|
@ -84,7 +84,7 @@ func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domai
|
|||
|
||||
func (r *CommentRepository) DeleteCommentList(ctx context.Context, commentID []string) error {
|
||||
// 批量删除指定id的comment,获取删除的总的数量、
|
||||
query := r.db.Model(&domain.Comment{}).Where("id IN (?)", commentID)
|
||||
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("id IN (?)", commentID)
|
||||
|
||||
if err := query.Delete(&domain.Comment{}).Error; err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -341,11 +341,12 @@ func (r *KnowledgeBaseRepository) CreateKnowledgeBase(ctx context.Context, maxKB
|
|||
Name: kb.Name,
|
||||
Type: domain.AppTypeWeb,
|
||||
Settings: domain.AppSettings{
|
||||
Title: kb.Name,
|
||||
Desc: kb.Name,
|
||||
Keyword: kb.Name,
|
||||
Icon: domain.DefaultPandaWikiIconB64,
|
||||
WelcomeStr: fmt.Sprintf("欢迎使用%s", kb.Name),
|
||||
Title: kb.Name,
|
||||
Desc: kb.Name,
|
||||
Keyword: kb.Name,
|
||||
AutoSitemap: true,
|
||||
Icon: domain.DefaultPandaWikiIconB64,
|
||||
WelcomeStr: fmt.Sprintf("欢迎使用%s", kb.Name),
|
||||
Btns: []any{
|
||||
AppBtn{
|
||||
ID: uuid.New().String(),
|
||||
|
|
|
|||
|
|
@ -683,7 +683,7 @@ func (r *NodeRepository) GetNodeReleaseListByKBID(ctx context.Context, kbID stri
|
|||
Where("kb_release_node_releases.kb_id = ?", kbID).
|
||||
Where("kb_release_node_releases.release_id = ?", kbRelease.ID).
|
||||
Where("nodes.permissions->>'visible' != ?", consts.NodeAccessPermClosed).
|
||||
Select("node_releases.node_id as id, node_releases.name, node_releases.type, node_releases.parent_id, node_releases.position, node_releases.meta->>'emoji' as emoji, node_releases.updated_at, nodes.permissions").
|
||||
Select("node_releases.node_id as id, node_releases.name, node_releases.type, node_releases.parent_id, nodes.position, node_releases.meta->>'emoji' as emoji, node_releases.updated_at, nodes.permissions").
|
||||
Find(&nodes).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,18 +60,14 @@ func (r *UserRepository) CreateUser(ctx context.Context, user *domain.User, edit
|
|||
}
|
||||
user.Password = string(hashedPassword)
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionFree {
|
||||
var count int64
|
||||
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if edition == consts.LicenseEditionFree && count >= 1 {
|
||||
return errors.New("free edition only allows 1 user")
|
||||
}
|
||||
if edition == consts.LicenseEditionContributor && count >= 5 {
|
||||
return errors.New("contributor edition only allows 5 user")
|
||||
}
|
||||
var count int64
|
||||
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count >= domain.GetBaseEditionLimitation(ctx).MaxAdmin {
|
||||
return fmt.Errorf("exceed max admin limit, current count: %d, max limit: %d", count, domain.GetBaseEditionLimitation(ctx).MaxAdmin)
|
||||
}
|
||||
|
||||
if err := tx.Create(user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,34 +88,38 @@ func NewAppUsecase(
|
|||
}
|
||||
|
||||
func (u *AppUsecase) ValidateUpdateApp(ctx context.Context, id string, req *domain.UpdateAppReq, edition consts.LicenseEdition) error {
|
||||
switch edition {
|
||||
case consts.LicenseEditionFree:
|
||||
app, err := u.repo.GetAppDetail(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if app.Settings.WatermarkContent != req.Settings.WatermarkContent ||
|
||||
app.Settings.WatermarkSetting != req.Settings.WatermarkSetting ||
|
||||
app.Settings.ContributeSettings != req.Settings.ContributeSettings ||
|
||||
app.Settings.CopySetting != req.Settings.CopySetting {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
case consts.LicenseEditionContributor:
|
||||
app, err := u.repo.GetAppDetail(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if app.Settings.WatermarkContent != req.Settings.WatermarkContent ||
|
||||
app.Settings.WatermarkSetting != req.Settings.WatermarkSetting ||
|
||||
app.Settings.CopySetting != req.Settings.CopySetting {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
case consts.LicenseEditionEnterprise:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported license type: %d", edition)
|
||||
app, err := u.repo.GetAppDetail(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
limitation := domain.GetBaseEditionLimitation(ctx)
|
||||
if !limitation.AllowCopyProtection && app.Settings.CopySetting != req.Settings.CopySetting {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if !limitation.AllowWatermark {
|
||||
if app.Settings.WatermarkSetting != req.Settings.WatermarkSetting || app.Settings.WatermarkContent != req.Settings.WatermarkContent {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
if !limitation.AllowAdvancedBot {
|
||||
if !slices.Equal(app.Settings.WechatServiceContainKeywords, req.Settings.WechatServiceContainKeywords) ||
|
||||
!slices.Equal(app.Settings.WechatServiceEqualKeywords, req.Settings.WechatServiceEqualKeywords) {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
if !limitation.AllowCommentAudit && app.Settings.WebAppCommentSettings.ModerationEnable != req.Settings.WebAppCommentSettings.ModerationEnable {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if !limitation.AllowOpenAIBotSettings {
|
||||
if app.Settings.OpenAIAPIBotSettings.IsEnabled != req.Settings.OpenAIAPIBotSettings.IsEnabled || app.Settings.OpenAIAPIBotSettings.SecretKey != req.Settings.OpenAIAPIBotSettings.SecretKey {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -618,8 +622,8 @@ func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId
|
|||
}
|
||||
showBrand := true
|
||||
defaultDisclaimer := "本回答由 PandaWiki 基于 AI 生成,仅供参考。"
|
||||
licenseEdition, _ := ctx.Value(consts.ContextKeyEdition).(consts.LicenseEdition)
|
||||
if licenseEdition < consts.LicenseEditionEnterprise {
|
||||
|
||||
if !domain.GetBaseEditionLimitation(ctx).AllowCustomCopyright {
|
||||
appInfo.Settings.WebAppCustomSettings.ShowBrandInfo = &showBrand
|
||||
appInfo.Settings.DisclaimerSettings.Content = &defaultDisclaimer
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ func (u *CommentUsecase) CreateComment(ctx context.Context, commentReq *domain.C
|
|||
return CommentStr, nil
|
||||
}
|
||||
|
||||
func (u *CommentUsecase) GetCommentListByNodeID(ctx context.Context, nodeID string, edition consts.LicenseEdition) (*domain.PaginatedResult[[]*domain.ShareCommentListItem], error) {
|
||||
comments, total, err := u.CommentRepo.GetCommentList(ctx, nodeID, edition)
|
||||
func (u *CommentUsecase) GetCommentListByNodeID(ctx context.Context, nodeID string) (*domain.PaginatedResult[[]*domain.ShareCommentListItem], error) {
|
||||
comments, total, err := u.CommentRepo.GetCommentList(ctx, nodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ func (u *NodeUsecase) GetNodePermissionsByID(ctx context.Context, id, kbID strin
|
|||
}
|
||||
|
||||
func (u *NodeUsecase) ValidateNodePermissionsEdit(req v1.NodePermissionEditReq, edition consts.LicenseEdition) error {
|
||||
if edition != consts.LicenseEditionEnterprise {
|
||||
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) {
|
||||
if req.Permissions.Answerable == consts.NodeAccessPermPartial || req.Permissions.Visitable == consts.NodeAccessPermPartial || req.Permissions.Visible == consts.NodeAccessPermPartial {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
|
|
@ -67,12 +68,12 @@ func (u *StatUseCase) ValidateStatDay(statDay consts.StatDay, edition consts.Lic
|
|||
case consts.StatDay1:
|
||||
return nil
|
||||
case consts.StatDay7:
|
||||
if edition < consts.LicenseEditionContributor {
|
||||
if edition == consts.LicenseEditionFree {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
case consts.StatDay30, consts.StatDay90:
|
||||
if edition < consts.LicenseEditionEnterprise {
|
||||
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
|
|
@ -144,7 +144,7 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
|
|||
callback();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [subscribe]);
|
||||
}, [subscribe, appPreviewData, id]);
|
||||
|
||||
return (
|
||||
<StyledCommonWrapper>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { setAppPreviewData } from '@/store/slices/config';
|
|||
import { DomainSocialMediaAccount } from '@/request/types';
|
||||
import Switch from '../basicComponents/Switch';
|
||||
import DragSocialInfo from '../basicComponents/DragSocialInfo';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface FooterConfigProps {
|
||||
data?: AppDetail | null;
|
||||
|
|
@ -75,9 +77,6 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
);
|
||||
const footer_show_intro = watch('footer_show_intro');
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
useEffect(() => {
|
||||
if (isEdit && appPreviewData) {
|
||||
setValue(
|
||||
|
|
@ -506,29 +505,33 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
)}
|
||||
/>
|
||||
</Stack>
|
||||
{isEnterprise && (
|
||||
<Stack direction={'column'} gap={2}>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
lineHeight: '22px',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 600,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: 4,
|
||||
height: 12,
|
||||
bgcolor: '#3248F2',
|
||||
borderRadius: '2px',
|
||||
mr: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
PandaWiki 版权信息
|
||||
</Box>
|
||||
|
||||
<Stack direction={'column'} gap={2}>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
lineHeight: '22px',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 600,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: 4,
|
||||
height: 12,
|
||||
bgcolor: '#3248F2',
|
||||
borderRadius: '2px',
|
||||
mr: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
PandaWiki 版权信息
|
||||
</Box>
|
||||
<VersionMask
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
sx={{ inset: '-8px 0' }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='show_brand_info'
|
||||
|
|
@ -548,7 +551,6 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
<Switch
|
||||
sx={{ marginLeft: 'auto' }}
|
||||
{...field}
|
||||
disabled={!isEnterprise}
|
||||
checked={field?.value === false ? false : true}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.checked);
|
||||
|
|
@ -558,8 +560,8 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
</Stack>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</VersionMask>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { KnowledgeBaseListItem } from '@/api';
|
||||
import { useURLSearchParams } from '@/hooks';
|
||||
import { useFeatureValue } from '@/hooks';
|
||||
import { ConstsUserRole } from '@/request/types';
|
||||
import { useAppDispatch, useAppSelector } from '@/store';
|
||||
import { setKbC, setKbId } from '@/store/slices/config';
|
||||
|
|
@ -23,14 +24,14 @@ const KBSelect = () => {
|
|||
|
||||
const dispatch = useAppDispatch();
|
||||
const [_, setSearchParams] = useURLSearchParams();
|
||||
const { kb_id, kbList, license, user } = useAppSelector(
|
||||
state => state.config,
|
||||
);
|
||||
const { kb_id, kbList, user } = useAppSelector(state => state.config);
|
||||
|
||||
const [modifyOpen, setModifyOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [opraData, setOpraData] = useState<KnowledgeBaseListItem | null>(null);
|
||||
|
||||
const wikiCount = useFeatureValue('wikiCount');
|
||||
|
||||
return (
|
||||
<>
|
||||
{(kbList || []).length > 0 && (
|
||||
|
|
@ -121,8 +122,7 @@ const KBSelect = () => {
|
|||
}}
|
||||
fullWidth
|
||||
disabled={
|
||||
(license.edition === 0 && (kbList || []).length >= 1) ||
|
||||
(license.edition === 1 && (kbList || []).length >= 3) ||
|
||||
(kbList || []).length >= wikiCount ||
|
||||
user.role === ConstsUserRole.UserRoleUser
|
||||
}
|
||||
onClick={event => {
|
||||
|
|
|
|||
|
|
@ -3,27 +3,20 @@ import {
|
|||
getApiV1License,
|
||||
deleteApiV1License,
|
||||
} from '@/request/pro/License';
|
||||
import { PostApiV1LicensePayload } from '@/request/pro/types';
|
||||
import HelpCenter from '@/assets/json/help-center.json';
|
||||
import Takeoff from '@/assets/json/takeoff.json';
|
||||
import error from '@/assets/json/error.json';
|
||||
import IconUpgrade from '@/assets/json/upgrade.json';
|
||||
import Upload from '@/components/UploadFile/Drag';
|
||||
import { EditionType } from '@/constant/enums';
|
||||
import { useVersionInfo } from '@/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '@/store';
|
||||
import { setLicense } from '@/store/slices/config';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Stack,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { Box, Button, IconButton, Stack, TextField } from '@mui/material';
|
||||
import { CusTabs, Icon, message, Modal } from '@ctzhian/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { useState } from 'react';
|
||||
import LottieIcon from '../LottieIcon';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
|
||||
interface AuthTypeModalProps {
|
||||
open: boolean;
|
||||
|
|
@ -42,10 +35,9 @@ const AuthTypeModal = ({
|
|||
const { license } = useAppSelector(state => state.config);
|
||||
|
||||
const [selected, setSelected] = useState<'file' | 'code'>(
|
||||
license.edition === 2 ? 'file' : 'code',
|
||||
);
|
||||
const [authVersion, setAuthVersion] = useState<'contributor' | 'enterprise'>(
|
||||
license.edition === 2 ? 'enterprise' : 'contributor',
|
||||
license.edition === ConstsLicenseEdition.LicenseEditionEnterprise
|
||||
? 'file'
|
||||
: 'code',
|
||||
);
|
||||
const [updateOpen, setUpdateOpen] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
|
|
@ -53,16 +45,15 @@ const AuthTypeModal = ({
|
|||
const [file, setFile] = useState<File | undefined>(undefined);
|
||||
const [unbindLoading, setUnbindLoading] = useState(false);
|
||||
|
||||
const versionInfo = useVersionInfo();
|
||||
|
||||
const handleSubmit = () => {
|
||||
const params: PostApiV1LicensePayload = {
|
||||
license_edition: authVersion,
|
||||
setLoading(true);
|
||||
postApiV1License({
|
||||
license_type: selected,
|
||||
license_code: code,
|
||||
license_file: file,
|
||||
};
|
||||
setLoading(true);
|
||||
|
||||
postApiV1License(params)
|
||||
})
|
||||
.then(() => {
|
||||
message.success('激活成功');
|
||||
setUpdateOpen(false);
|
||||
|
|
@ -148,10 +139,8 @@ const AuthTypeModal = ({
|
|||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<Box sx={{ width: 120, flexShrink: 0 }}>产品型号</Box>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={2}>
|
||||
<Box sx={{ minWidth: 50 }}>
|
||||
{EditionType[license.edition as keyof typeof EditionType].text}
|
||||
</Box>
|
||||
{license.edition === 0 ? (
|
||||
<Box sx={{ minWidth: 50 }}>{versionInfo.label}</Box>
|
||||
{license.edition === ConstsLicenseEdition.LicenseEditionFree ? (
|
||||
<Stack direction={'row'} gap={2}>
|
||||
<Button
|
||||
size='small'
|
||||
|
|
@ -240,7 +229,7 @@ const AuthTypeModal = ({
|
|||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
{license.edition! > 0 && (
|
||||
{license.edition! !== ConstsLicenseEdition.LicenseEditionFree && (
|
||||
<Box>
|
||||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<Box sx={{ width: 120, flexShrink: 0 }}>授权时间</Box>
|
||||
|
|
@ -288,18 +277,6 @@ const AuthTypeModal = ({
|
|||
value={selected}
|
||||
change={(v: string) => setSelected(v as 'file' | 'code')}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
sx={{ mt: 2 }}
|
||||
value={authVersion}
|
||||
onChange={e =>
|
||||
setAuthVersion(e.target.value as 'contributor' | 'enterprise')
|
||||
}
|
||||
>
|
||||
<MenuItem value='contributor'>联创版</MenuItem>
|
||||
<MenuItem value='enterprise'>企业版</MenuItem>
|
||||
</TextField>
|
||||
{selected === 'code' && (
|
||||
<TextField
|
||||
sx={{ mt: 2 }}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,14 @@
|
|||
import HelpCenter from '@/assets/json/help-center.json';
|
||||
import IconUpgrade from '@/assets/json/upgrade.json';
|
||||
import LottieIcon from '@/components/LottieIcon';
|
||||
import { EditionType } from '@/constant/enums';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { Box, Stack, Tooltip } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import packageJson from '../../../package.json';
|
||||
import AuthTypeModal from './AuthTypeModal';
|
||||
import freeVersion from '@/assets/images/free-version.png';
|
||||
import enterpriseVersion from '@/assets/images/enterprise-version.png';
|
||||
import contributorVersion from '@/assets/images/contributor-version.png';
|
||||
|
||||
const versionMap = {
|
||||
0: freeVersion,
|
||||
1: contributorVersion,
|
||||
2: enterpriseVersion,
|
||||
};
|
||||
import { useVersionInfo } from '@/hooks';
|
||||
|
||||
const Version = () => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
const versionInfo = useVersionInfo();
|
||||
const curVersion = import.meta.env.VITE_APP_VERSION || packageJson.version;
|
||||
const [latestVersion, setLatestVersion] = useState<string | undefined>(
|
||||
undefined,
|
||||
|
|
@ -57,11 +47,8 @@ const Version = () => {
|
|||
>
|
||||
<Stack direction={'row'} alignItems='center' gap={0.5}>
|
||||
<Box sx={{ width: 30, color: 'text.tertiary' }}>型号</Box>
|
||||
<img
|
||||
src={versionMap[license.edition!]}
|
||||
style={{ height: 13, marginTop: -1 }}
|
||||
/>
|
||||
{EditionType[license.edition as keyof typeof EditionType].text}
|
||||
<img src={versionInfo.image} style={{ height: 13, marginTop: -1 }} />
|
||||
{versionInfo.label}
|
||||
</Stack>
|
||||
<Stack direction={'row'} gap={0.5}>
|
||||
<Box sx={{ width: 30, color: 'text.tertiary' }}>版本</Box>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import { Modal, message } from '@ctzhian/ui';
|
|||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import { ConstsUserKBPermission, V1KBUserInviteReq } from '@/request/types';
|
||||
import { ConstsLicenseEdition } from '@/request/pro/types';
|
||||
|
||||
|
|
@ -26,9 +27,13 @@ const VERSION_MAP = {
|
|||
message: '开源版只支持 1 个管理员',
|
||||
max: 1,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionContributor]: {
|
||||
message: '联创版最多支持 3 个管理员',
|
||||
max: 3,
|
||||
[ConstsLicenseEdition.LicenseEditionProfession]: {
|
||||
message: '专业版最多支持 20 个管理员',
|
||||
max: 20,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionBusiness]: {
|
||||
message: '商业版最多支持 50 个管理员',
|
||||
max: 50,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -45,9 +50,6 @@ const MemberAdd = ({
|
|||
const { kbList, license, refreshAdminRequest } = useAppSelector(
|
||||
state => state.config,
|
||||
);
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const {
|
||||
control,
|
||||
|
|
@ -118,6 +120,10 @@ const MemberAdd = ({
|
|||
});
|
||||
});
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
|
@ -253,6 +259,14 @@ const MemberAdd = ({
|
|||
fullWidth
|
||||
displayEmpty
|
||||
sx={{ height: 52 }}
|
||||
MenuProps={{
|
||||
sx: {
|
||||
'.Mui-disabled': {
|
||||
opacity: 1,
|
||||
color: 'text.disabled',
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderValue={(value: V1KBUserInviteReq['perm']) => {
|
||||
return value ? (
|
||||
PERM_MAP[value]
|
||||
|
|
@ -266,17 +280,25 @@ const MemberAdd = ({
|
|||
>
|
||||
完全控制
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||
disabled={!isPro}
|
||||
>
|
||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
||||
文档管理{' '}
|
||||
<VersionCanUse
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
/>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||
disabled={!isPro}
|
||||
>
|
||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
||||
数据运营{' '}
|
||||
<VersionCanUse
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
/>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
import { styled } from '@mui/material';
|
||||
import { useVersionInfo } from '@/hooks';
|
||||
import { VersionInfoMap } from '@/constant/version';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
import { SxProps } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const StyledMaskWrapper = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const StyledMask = styled('div')(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
inset: -8,
|
||||
zIndex: 99,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flex: 1,
|
||||
borderRadius: '10px',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
background: 'rgba(241,242,248,0.8)',
|
||||
backdropFilter: 'blur(0.5px)',
|
||||
}));
|
||||
|
||||
const StyledMaskContent = styled('div')(({ theme }) => ({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
const StyledMaskVersion = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(0.5),
|
||||
padding: theme.spacing(0.5, 1),
|
||||
backgroundColor: theme.palette.background.paper3,
|
||||
borderRadius: '10px',
|
||||
fontSize: 12,
|
||||
lineHeight: 1,
|
||||
color: theme.palette.light.main,
|
||||
}));
|
||||
|
||||
const VersionMask = ({
|
||||
permission = [
|
||||
ConstsLicenseEdition.LicenseEditionFree,
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
],
|
||||
children,
|
||||
sx,
|
||||
}: {
|
||||
permission?: ConstsLicenseEdition[];
|
||||
children?: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
const versionInfo = useVersionInfo();
|
||||
const hasPermission = permission.includes(versionInfo.permission);
|
||||
if (hasPermission) return children;
|
||||
const nextVersionInfo = VersionInfoMap[permission[0]];
|
||||
|
||||
return (
|
||||
<StyledMaskWrapper>
|
||||
{children}
|
||||
<StyledMask sx={sx}>
|
||||
<StyledMaskContent>
|
||||
<StyledMaskVersion sx={{ backgroundColor: nextVersionInfo.bgColor }}>
|
||||
<img
|
||||
src={nextVersionInfo.image}
|
||||
style={{ width: 12, objectFit: 'contain', marginTop: 1 }}
|
||||
alt={nextVersionInfo.label}
|
||||
/>
|
||||
{nextVersionInfo?.label}可用
|
||||
</StyledMaskVersion>
|
||||
</StyledMaskContent>
|
||||
</StyledMask>
|
||||
</StyledMaskWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const VersionCanUse = ({
|
||||
permission = [
|
||||
ConstsLicenseEdition.LicenseEditionFree,
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
],
|
||||
sx,
|
||||
}: {
|
||||
permission?: ConstsLicenseEdition[];
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
const versionInfo = useVersionInfo();
|
||||
const hasPermission = permission.includes(versionInfo.permission);
|
||||
if (hasPermission) return null;
|
||||
const nextVersionInfo = VersionInfoMap[permission[0]];
|
||||
return (
|
||||
<StyledMaskContent sx={{ width: 'auto', ml: 1, ...sx }}>
|
||||
<StyledMaskVersion sx={{ backgroundColor: nextVersionInfo.bgColor }}>
|
||||
<img
|
||||
src={nextVersionInfo.image}
|
||||
style={{ width: 12, objectFit: 'contain', marginTop: 1 }}
|
||||
alt={nextVersionInfo.label}
|
||||
/>
|
||||
{nextVersionInfo?.label}可用
|
||||
</StyledMaskVersion>
|
||||
</StyledMaskContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default VersionMask;
|
||||
|
|
@ -797,21 +797,6 @@ export const FeedbackType = {
|
|||
3: '其他',
|
||||
};
|
||||
|
||||
export const Free = 0;
|
||||
export const Contributor = 1;
|
||||
export const Enterprise = 2;
|
||||
export const EditionType = {
|
||||
[Free]: {
|
||||
text: '开源版',
|
||||
},
|
||||
[Contributor]: {
|
||||
text: '联创版',
|
||||
},
|
||||
[Enterprise]: {
|
||||
text: '企业版',
|
||||
},
|
||||
};
|
||||
|
||||
export const DocWidth = {
|
||||
full: {
|
||||
label: '全屏',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,293 @@
|
|||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
|
||||
import freeVersion from '@/assets/images/free-version.png';
|
||||
import proVersion from '@/assets/images/pro-version.png';
|
||||
import businessVersion from '@/assets/images/business-version.png';
|
||||
import enterpriseVersion from '@/assets/images/enterprise-version.png';
|
||||
|
||||
export const PROFESSION_VERSION_PERMISSION = [
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
];
|
||||
|
||||
export const BUSINESS_VERSION_PERMISSION = [
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
];
|
||||
|
||||
export const ENTERPRISE_VERSION_PERMISSION = [
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
];
|
||||
|
||||
export const VersionInfoMap = {
|
||||
[ConstsLicenseEdition.LicenseEditionFree]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionFree,
|
||||
label: '开源版',
|
||||
image: freeVersion,
|
||||
bgColor: '#8E9DAC',
|
||||
nextVersion: ConstsLicenseEdition.LicenseEditionProfession,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionProfession]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionProfession,
|
||||
label: '专业版',
|
||||
image: proVersion,
|
||||
bgColor: '#0933BA',
|
||||
nextVersion: ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionBusiness]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
label: '商业版',
|
||||
image: businessVersion,
|
||||
bgColor: '#382A79',
|
||||
nextVersion: ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionEnterprise]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
label: '企业版',
|
||||
image: enterpriseVersion,
|
||||
bgColor: '#21222D',
|
||||
nextVersion: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 功能支持状态
|
||||
*/
|
||||
export enum FeatureStatus {
|
||||
/** 不支持 */
|
||||
NOT_SUPPORTED = 'not_supported',
|
||||
/** 支持 */
|
||||
SUPPORTED = 'supported',
|
||||
/** 基础配置 */
|
||||
BASIC = 'basic',
|
||||
/** 高级配置 */
|
||||
ADVANCED = 'advanced',
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本信息配置
|
||||
*/
|
||||
export interface VersionInfo {
|
||||
/** 版本名称 */
|
||||
label: string;
|
||||
/** 功能特性 */
|
||||
features: {
|
||||
/** Wiki 站点数量 */
|
||||
wikiCount: number;
|
||||
/** 每个 Wiki 的文档数量 */
|
||||
docCountPerWiki: number;
|
||||
/** 管理员数量 */
|
||||
adminCount: number;
|
||||
/** 管理员分权控制 */
|
||||
adminPermissionControl: FeatureStatus;
|
||||
/** SEO 配置 */
|
||||
seoConfig: FeatureStatus;
|
||||
/** 多语言支持 */
|
||||
multiLanguage: FeatureStatus;
|
||||
/** 自定义版权信息 */
|
||||
customCopyright: FeatureStatus;
|
||||
/** 访问流量分析 */
|
||||
trafficAnalysis: FeatureStatus;
|
||||
/** 自定义 AI 提示词 */
|
||||
customAIPrompt: FeatureStatus;
|
||||
/** SSO 登录 */
|
||||
ssoLogin: number;
|
||||
/** 访客权限控制 */
|
||||
visitorPermissionControl: FeatureStatus;
|
||||
/** 页面水印 */
|
||||
pageWatermark: FeatureStatus;
|
||||
/** 内容不可复制 */
|
||||
contentNoCopy: FeatureStatus;
|
||||
/** 敏感内容过滤 */
|
||||
sensitiveContentFilter: FeatureStatus;
|
||||
/** 网页挂件机器人 */
|
||||
webWidgetRobot: FeatureStatus;
|
||||
/** 飞书问答机器人 */
|
||||
feishuQARobot: FeatureStatus;
|
||||
/** 钉钉问答机器人 */
|
||||
dingtalkQARobot: FeatureStatus;
|
||||
/** 企业微信问答机器人 */
|
||||
wecomQARobot: FeatureStatus;
|
||||
/** 企业微信客服机器人 */
|
||||
wecomServiceRobot: FeatureStatus;
|
||||
/** Discord 问答机器人 */
|
||||
discordQARobot: FeatureStatus;
|
||||
/** 文档历史版本管理 */
|
||||
docVersionHistory: FeatureStatus;
|
||||
/** API 调用 */
|
||||
apiCall: FeatureStatus;
|
||||
/** 项目源码 */
|
||||
sourceCode: FeatureStatus;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本信息映射
|
||||
*/
|
||||
export const VERSION_INFO: Record<ConstsLicenseEdition, VersionInfo> = {
|
||||
[ConstsLicenseEdition.LicenseEditionFree]: {
|
||||
label: '开源版',
|
||||
features: {
|
||||
wikiCount: 1,
|
||||
docCountPerWiki: 300,
|
||||
adminCount: 1,
|
||||
adminPermissionControl: FeatureStatus.NOT_SUPPORTED,
|
||||
seoConfig: FeatureStatus.BASIC,
|
||||
multiLanguage: FeatureStatus.NOT_SUPPORTED,
|
||||
customCopyright: FeatureStatus.NOT_SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.BASIC,
|
||||
customAIPrompt: FeatureStatus.NOT_SUPPORTED,
|
||||
ssoLogin: 0,
|
||||
visitorPermissionControl: FeatureStatus.NOT_SUPPORTED,
|
||||
pageWatermark: FeatureStatus.NOT_SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.NOT_SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.NOT_SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.BASIC,
|
||||
feishuQARobot: FeatureStatus.BASIC,
|
||||
dingtalkQARobot: FeatureStatus.BASIC,
|
||||
wecomQARobot: FeatureStatus.BASIC,
|
||||
wecomServiceRobot: FeatureStatus.BASIC,
|
||||
discordQARobot: FeatureStatus.BASIC,
|
||||
docVersionHistory: FeatureStatus.NOT_SUPPORTED,
|
||||
apiCall: FeatureStatus.NOT_SUPPORTED,
|
||||
sourceCode: FeatureStatus.SUPPORTED,
|
||||
},
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionProfession]: {
|
||||
label: '专业版',
|
||||
features: {
|
||||
wikiCount: 10,
|
||||
docCountPerWiki: 10000,
|
||||
adminCount: 20,
|
||||
adminPermissionControl: FeatureStatus.SUPPORTED,
|
||||
seoConfig: FeatureStatus.ADVANCED,
|
||||
multiLanguage: FeatureStatus.SUPPORTED,
|
||||
customCopyright: FeatureStatus.SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.ADVANCED,
|
||||
customAIPrompt: FeatureStatus.SUPPORTED,
|
||||
ssoLogin: 0,
|
||||
visitorPermissionControl: FeatureStatus.NOT_SUPPORTED,
|
||||
pageWatermark: FeatureStatus.NOT_SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.NOT_SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.NOT_SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.ADVANCED,
|
||||
feishuQARobot: FeatureStatus.ADVANCED,
|
||||
dingtalkQARobot: FeatureStatus.ADVANCED,
|
||||
wecomQARobot: FeatureStatus.ADVANCED,
|
||||
wecomServiceRobot: FeatureStatus.ADVANCED,
|
||||
discordQARobot: FeatureStatus.ADVANCED,
|
||||
docVersionHistory: FeatureStatus.NOT_SUPPORTED,
|
||||
apiCall: FeatureStatus.NOT_SUPPORTED,
|
||||
sourceCode: FeatureStatus.NOT_SUPPORTED,
|
||||
},
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionBusiness]: {
|
||||
label: '商业版',
|
||||
features: {
|
||||
wikiCount: 20,
|
||||
docCountPerWiki: 10000,
|
||||
adminCount: 50,
|
||||
adminPermissionControl: FeatureStatus.SUPPORTED,
|
||||
seoConfig: FeatureStatus.ADVANCED,
|
||||
multiLanguage: FeatureStatus.SUPPORTED,
|
||||
customCopyright: FeatureStatus.SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.ADVANCED,
|
||||
customAIPrompt: FeatureStatus.SUPPORTED,
|
||||
ssoLogin: 2000,
|
||||
visitorPermissionControl: FeatureStatus.SUPPORTED,
|
||||
pageWatermark: FeatureStatus.SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.ADVANCED,
|
||||
feishuQARobot: FeatureStatus.ADVANCED,
|
||||
dingtalkQARobot: FeatureStatus.ADVANCED,
|
||||
wecomQARobot: FeatureStatus.ADVANCED,
|
||||
wecomServiceRobot: FeatureStatus.ADVANCED,
|
||||
discordQARobot: FeatureStatus.ADVANCED,
|
||||
docVersionHistory: FeatureStatus.SUPPORTED,
|
||||
apiCall: FeatureStatus.SUPPORTED,
|
||||
sourceCode: FeatureStatus.NOT_SUPPORTED,
|
||||
},
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionEnterprise]: {
|
||||
label: '企业版',
|
||||
features: {
|
||||
wikiCount: Infinity,
|
||||
docCountPerWiki: Infinity,
|
||||
adminCount: Infinity,
|
||||
adminPermissionControl: FeatureStatus.SUPPORTED,
|
||||
seoConfig: FeatureStatus.ADVANCED,
|
||||
multiLanguage: FeatureStatus.SUPPORTED,
|
||||
customCopyright: FeatureStatus.SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.ADVANCED,
|
||||
customAIPrompt: FeatureStatus.SUPPORTED,
|
||||
ssoLogin: Infinity,
|
||||
visitorPermissionControl: FeatureStatus.SUPPORTED,
|
||||
pageWatermark: FeatureStatus.SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.ADVANCED,
|
||||
feishuQARobot: FeatureStatus.ADVANCED,
|
||||
dingtalkQARobot: FeatureStatus.ADVANCED,
|
||||
wecomQARobot: FeatureStatus.ADVANCED,
|
||||
wecomServiceRobot: FeatureStatus.ADVANCED,
|
||||
discordQARobot: FeatureStatus.ADVANCED,
|
||||
docVersionHistory: FeatureStatus.SUPPORTED,
|
||||
apiCall: FeatureStatus.SUPPORTED,
|
||||
sourceCode: FeatureStatus.SUPPORTED,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 功能特性标签映射
|
||||
*/
|
||||
export const FEATURE_LABELS: Record<string, string> = {
|
||||
wikiCount: 'Wiki 站点数量',
|
||||
docCountPerWiki: '每个 Wiki 的文档数量',
|
||||
adminCount: '管理员数量',
|
||||
adminPermissionControl: '管理员分权控制',
|
||||
seoConfig: 'SEO 配置',
|
||||
multiLanguage: '多语言支持',
|
||||
customCopyright: '自定义版权信息',
|
||||
trafficAnalysis: '访问流量分析',
|
||||
customAIPrompt: '自定义 AI 提示词',
|
||||
ssoLogin: 'SSO 登录',
|
||||
visitorPermissionControl: '访客权限控制',
|
||||
pageWatermark: '页面水印',
|
||||
contentNoCopy: '内容不可复制',
|
||||
sensitiveContentFilter: '敏感内容过滤',
|
||||
webWidgetRobot: '网页挂件机器人',
|
||||
feishuQARobot: '飞书问答机器人',
|
||||
dingtalkQARobot: '钉钉问答机器人',
|
||||
wecomQARobot: '企业微信问答机器人',
|
||||
wecomServiceRobot: '企业微信客服机器人',
|
||||
discordQARobot: 'Discord 问答机器人',
|
||||
docVersionHistory: '文档历史版本管理',
|
||||
apiCall: 'API 调用',
|
||||
sourceCode: '项目源码',
|
||||
};
|
||||
|
||||
/**
|
||||
* 功能状态显示文本映射
|
||||
*/
|
||||
export const FEATURE_STATUS_LABELS: Record<FeatureStatus, string> = {
|
||||
[FeatureStatus.NOT_SUPPORTED]: '不支持',
|
||||
[FeatureStatus.SUPPORTED]: '支持',
|
||||
[FeatureStatus.BASIC]: '基础配置',
|
||||
[FeatureStatus.ADVANCED]: '高级配置',
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取功能特性值
|
||||
*/
|
||||
export function getFeatureValue<K extends keyof VersionInfo['features']>(
|
||||
edition: ConstsLicenseEdition,
|
||||
key: K,
|
||||
): VersionInfo['features'][K] {
|
||||
return (
|
||||
VERSION_INFO[edition] ||
|
||||
VERSION_INFO[ConstsLicenseEdition.LicenseEditionFree]
|
||||
).features[key];
|
||||
}
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
export { useBindCaptcha } from './useBindCaptcha';
|
||||
export { useCommitPendingInput } from './useCommitPendingInput';
|
||||
export { useURLSearchParams } from './useURLSearchParams';
|
||||
export {
|
||||
useFeatureValue,
|
||||
useFeatureValueSupported,
|
||||
useVersionInfo,
|
||||
} from './useVersionFeature';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
FeatureStatus,
|
||||
VersionInfoMap,
|
||||
VersionInfo,
|
||||
getFeatureValue,
|
||||
} from '@/constant/version';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
export const useFeatureValue = <K extends keyof VersionInfo['features']>(
|
||||
key: K,
|
||||
): VersionInfo['features'][K] => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
return getFeatureValue(license.edition!, key);
|
||||
};
|
||||
|
||||
export const useFeatureValueSupported = (
|
||||
key: keyof VersionInfo['features'],
|
||||
) => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
return (
|
||||
getFeatureValue(license.edition!, key) === FeatureStatus.SUPPORTED ||
|
||||
getFeatureValue(license.edition!, key) === FeatureStatus.ADVANCED
|
||||
);
|
||||
};
|
||||
|
||||
export const useVersionInfo = () => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
return (
|
||||
VersionInfoMap[
|
||||
license.edition ?? ConstsLicenseEdition.LicenseEditionFree
|
||||
] || VersionInfoMap[ConstsLicenseEdition.LicenseEditionFree]
|
||||
);
|
||||
};
|
||||
|
|
@ -8,6 +8,8 @@ import { styled } from '@mui/material/styles';
|
|||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
import DocModal from './DocModal';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
import { useURLSearchParams } from '@/hooks';
|
||||
import {
|
||||
|
|
@ -46,7 +48,7 @@ const statusColorMap = {
|
|||
} as const;
|
||||
|
||||
export default function ContributionPage() {
|
||||
const { kb_id = '', kbDetail } = useAppSelector(state => state.config);
|
||||
const { kb_id = '', license } = useAppSelector(state => state.config);
|
||||
const [searchParams, setSearchParams] = useURLSearchParams();
|
||||
const page = Number(searchParams.get('page') || '1');
|
||||
const pageSize = Number(searchParams.get('page_size') || '20');
|
||||
|
|
@ -283,111 +285,114 @@ export default function ContributionPage() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (kb_id) getData();
|
||||
if (kb_id && PROFESSION_VERSION_PERMISSION.includes(license.edition!))
|
||||
getData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, pageSize, nodeNameParam, authNameParam, kb_id]);
|
||||
}, [page, pageSize, nodeNameParam, authNameParam, kb_id, license.edition]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
sx={{ p: 2 }}
|
||||
>
|
||||
<StyledSearchRow direction='row' sx={{ p: 0, flex: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='文档'
|
||||
value={searchDoc}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ node_name: searchDoc || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ node_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchDoc(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='用户'
|
||||
value={searchUser}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ auth_name: searchUser || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ auth_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchUser(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
</StyledSearchRow>
|
||||
</Stack>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey='id'
|
||||
height='calc(100vh - 148px)'
|
||||
size='small'
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
...tableSx,
|
||||
'.MuiTableContainer-root': {
|
||||
height: 'calc(100vh - 148px - 70px)',
|
||||
},
|
||||
}}
|
||||
pagination={{
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
onChange: (page, pageSize) => {
|
||||
setSearchParams({
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
});
|
||||
},
|
||||
}}
|
||||
PaginationProps={{
|
||||
sx: {
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
p: 2,
|
||||
'.MuiSelect-root': {
|
||||
width: 100,
|
||||
<VersionMask permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
sx={{ p: 2 }}
|
||||
>
|
||||
<StyledSearchRow direction='row' sx={{ p: 0, flex: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='文档'
|
||||
value={searchDoc}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ node_name: searchDoc || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ node_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchDoc(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='用户'
|
||||
value={searchUser}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ auth_name: searchUser || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ auth_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchUser(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
</StyledSearchRow>
|
||||
</Stack>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey='id'
|
||||
height='calc(100vh - 148px)'
|
||||
size='small'
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
...tableSx,
|
||||
'.MuiTableContainer-root': {
|
||||
height: 'calc(100vh - 148px - 70px)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
pagination={{
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
onChange: (page, pageSize) => {
|
||||
setSearchParams({
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
});
|
||||
},
|
||||
}}
|
||||
PaginationProps={{
|
||||
sx: {
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
p: 2,
|
||||
'.MuiSelect-root': {
|
||||
width: 100,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{previewRow?.meta?.content_type === 'md' ? (
|
||||
<MarkdownPreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
{previewRow?.meta?.content_type === 'md' ? (
|
||||
<MarkdownPreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
) : (
|
||||
<ContributePreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
)}
|
||||
<DocModal
|
||||
open={docModalOpen}
|
||||
onClose={() => setDocModalOpen(false)}
|
||||
onOk={handleDocModalOk}
|
||||
/>
|
||||
) : (
|
||||
<ContributePreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
)}
|
||||
<DocModal
|
||||
open={docModalOpen}
|
||||
onClose={() => setDocModalOpen(false)}
|
||||
onOk={handleDocModalOk}
|
||||
/>
|
||||
</VersionMask>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import {
|
|||
import dayjs from 'dayjs';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
|
||||
interface DocPropertiesModalProps {
|
||||
open: boolean;
|
||||
|
|
@ -40,8 +42,6 @@ interface DocPropertiesModalProps {
|
|||
data: DomainNodeListItemResp[];
|
||||
}
|
||||
|
||||
const tips = '(企业版可用)';
|
||||
|
||||
const StyledText = styled('div')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 16,
|
||||
|
|
@ -53,7 +53,12 @@ const PER_OPTIONS = [
|
|||
value: ConstsNodeAccessPerm.NodeAccessPermOpen,
|
||||
},
|
||||
{
|
||||
label: '部分开放',
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<span>部分开放</span>
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: ConstsNodeAccessPerm.NodeAccessPermPartial,
|
||||
},
|
||||
{
|
||||
|
|
@ -128,13 +133,13 @@ const DocPropertiesModal = ({
|
|||
visitable: values.visitable as ConstsNodeAccessPerm,
|
||||
visible: values.visible as ConstsNodeAccessPerm,
|
||||
},
|
||||
answerable_groups: isEnterprise
|
||||
answerable_groups: isBusiness
|
||||
? values.answerable_groups.map(item => item.id!)
|
||||
: undefined,
|
||||
visitable_groups: isEnterprise
|
||||
visitable_groups: isBusiness
|
||||
? values.visitable_groups.map(item => item.id!)
|
||||
: undefined,
|
||||
visible_groups: isEnterprise
|
||||
visible_groups: isBusiness
|
||||
? values.visible_groups.map(item => item.id!)
|
||||
: undefined,
|
||||
}),
|
||||
|
|
@ -153,15 +158,15 @@ const DocPropertiesModal = ({
|
|||
});
|
||||
});
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const tree = filterEmptyFolders(convertToTree(data));
|
||||
|
||||
useEffect(() => {
|
||||
if (open && data) {
|
||||
if (isEnterprise) {
|
||||
if (isBusiness) {
|
||||
getApiProV1AuthGroupList({
|
||||
kb_id: kb_id!,
|
||||
page: 1,
|
||||
|
|
@ -206,7 +211,7 @@ const DocPropertiesModal = ({
|
|||
);
|
||||
});
|
||||
}
|
||||
}, [open, data, isEnterprise]);
|
||||
}, [open, data, isBusiness]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
|
|
@ -302,22 +307,15 @@ const DocPropertiesModal = ({
|
|||
name='answerable'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup row {...field}>
|
||||
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||
{PER_OPTIONS.map(option => (
|
||||
<FormControlLabel
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
option.label +
|
||||
(!isEnterprise &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
? tips
|
||||
: '')
|
||||
}
|
||||
label={option.label}
|
||||
disabled={
|
||||
!isEnterprise &&
|
||||
!isBusiness &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
}
|
||||
|
|
@ -359,22 +357,15 @@ const DocPropertiesModal = ({
|
|||
name='visitable'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup row {...field}>
|
||||
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||
{PER_OPTIONS.map(option => (
|
||||
<FormControlLabel
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
option.label +
|
||||
(!isEnterprise &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
? tips
|
||||
: '')
|
||||
}
|
||||
label={option.label}
|
||||
disabled={
|
||||
!isEnterprise &&
|
||||
!isBusiness &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
}
|
||||
|
|
@ -416,22 +407,15 @@ const DocPropertiesModal = ({
|
|||
name='visible'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup row {...field}>
|
||||
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||
{PER_OPTIONS.map(option => (
|
||||
<FormControlLabel
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
option.label +
|
||||
(!isEnterprise &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
? tips
|
||||
: '')
|
||||
}
|
||||
label={option.label}
|
||||
disabled={
|
||||
!isEnterprise &&
|
||||
!isBusiness &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import { useNavigate, useOutletContext } from 'react-router-dom';
|
|||
import { WrapContext } from '..';
|
||||
import DocAddByCustomText from '../../component/DocAddByCustomText';
|
||||
import DocDelete from '../../component/DocDelete';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
|
||||
interface HeaderProps {
|
||||
edit: boolean;
|
||||
|
|
@ -52,8 +54,8 @@ const Header = ({
|
|||
|
||||
const [showSaveTip, setShowSaveTip] = useState(false);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const handlePublish = useCallback(() => {
|
||||
|
|
@ -309,6 +311,7 @@ const Header = ({
|
|||
// },
|
||||
{
|
||||
key: 'copy',
|
||||
textSx: { flex: 1 },
|
||||
label: <StyledMenuSelect>复制</StyledMenuSelect>,
|
||||
onClick: () => {
|
||||
if (kb_id) {
|
||||
|
|
@ -328,26 +331,22 @@ const Header = ({
|
|||
},
|
||||
{
|
||||
key: 'version',
|
||||
textSx: { flex: 1 },
|
||||
label: (
|
||||
<StyledMenuSelect disabled={!isEnterprise}>
|
||||
历史版本{' '}
|
||||
{!isEnterprise && (
|
||||
<Tooltip title='企业版可用' placement='top' arrow>
|
||||
<InfoIcon
|
||||
sx={{ color: 'text.secondary', fontSize: 14 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<StyledMenuSelect disabled={!isBusiness}>
|
||||
历史版本
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</StyledMenuSelect>
|
||||
),
|
||||
onClick: () => {
|
||||
if (isEnterprise) {
|
||||
if (isBusiness) {
|
||||
navigate(`/doc/editor/history/${detail.id}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'rename',
|
||||
textSx: { flex: 1 },
|
||||
label: <StyledMenuSelect>重命名</StyledMenuSelect>,
|
||||
onClick: () => {
|
||||
setRenameOpen(true);
|
||||
|
|
@ -355,6 +354,7 @@ const Header = ({
|
|||
},
|
||||
{
|
||||
key: 'delete',
|
||||
textSx: { flex: 1 },
|
||||
label: <StyledMenuSelect>删除</StyledMenuSelect>,
|
||||
onClick: () => {
|
||||
setDelOpen(true);
|
||||
|
|
@ -566,7 +566,7 @@ const StyledMenuSelect = styled('div')<{ disabled?: boolean }>(
|
|||
padding: theme.spacing(0, 2),
|
||||
lineHeight: '40px',
|
||||
height: 40,
|
||||
width: 106,
|
||||
minWidth: 106,
|
||||
borderRadius: '5px',
|
||||
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import Header from './Header';
|
|||
import Summary from './Summary';
|
||||
import Toc from './Toc';
|
||||
import Toolbar from './Toolbar';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface WrapProps {
|
||||
detail: V1NodeDetailResp;
|
||||
|
|
@ -72,8 +73,8 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
|||
emoji: defaultDetail.meta?.emoji || '',
|
||||
});
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const debouncedUpdateSummary = useCallback(
|
||||
|
|
@ -383,7 +384,7 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
|||
</Stack>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip arrow title={isEnterprise ? '查看历史版本' : ''}>
|
||||
<Tooltip arrow title={isBusiness ? '查看历史版本' : ''}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
|
|
@ -391,13 +392,13 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
|||
sx={{
|
||||
fontSize: 12,
|
||||
color: 'text.tertiary',
|
||||
cursor: isEnterprise ? 'pointer' : 'text',
|
||||
cursor: isBusiness ? 'pointer' : 'text',
|
||||
':hover': {
|
||||
color: isEnterprise ? 'primary.main' : 'text.tertiary',
|
||||
color: isBusiness ? 'primary.main' : 'text.tertiary',
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isEnterprise) {
|
||||
if (isBusiness) {
|
||||
navigate(`/doc/editor/history/${defaultDetail.id}`);
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
IconButton,
|
||||
Stack,
|
||||
useTheme,
|
||||
ButtonBase,
|
||||
} from '@mui/material';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import VersionPublish from '../release/components/VersionPublish';
|
||||
|
|
@ -418,15 +419,19 @@ const Content = () => {
|
|||
>
|
||||
{publish.unpublished} 个 文档/文件夹未发布,
|
||||
</Box>
|
||||
<Button
|
||||
size='small'
|
||||
sx={{ minWidth: 0, p: 0, fontSize: 12 }}
|
||||
<ButtonBase
|
||||
disableRipple
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
color: 'primary.main',
|
||||
}}
|
||||
onClick={() => {
|
||||
setPublishOpen(true);
|
||||
}}
|
||||
>
|
||||
去发布
|
||||
</Button>
|
||||
</ButtonBase>
|
||||
</>
|
||||
)}
|
||||
{ragReStartCount > 0 && (
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
ButtonBase,
|
||||
} from '@mui/material';
|
||||
import { Ellipsis, Table, Modal, Icon, message } from '@ctzhian/ui';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
|
||||
|
|
@ -162,8 +163,8 @@ const Comments = ({
|
|||
useState<DomainWebAppCommentSettings | null>(null);
|
||||
|
||||
const isEnableReview = useMemo(() => {
|
||||
return !!(license.edition === 1 || license.edition === 2);
|
||||
}, [license]);
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCommentsFilter(isEnableReview);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import dayjs from 'dayjs';
|
|||
import { ColumnType } from '@ctzhian/ui/dist/Table';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface AddRoleProps {
|
||||
open: boolean;
|
||||
|
|
@ -23,7 +25,8 @@ interface AddRoleProps {
|
|||
}
|
||||
|
||||
const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
||||
const { kb_id, license } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
const [list, setList] = useState<V1UserListItemResp[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string>('');
|
||||
|
|
@ -31,10 +34,6 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
|||
ConstsUserKBPermission.UserKBPermissionFullControl,
|
||||
);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const columns: ColumnType<V1UserListItemResp>[] = [
|
||||
{
|
||||
title: '',
|
||||
|
|
@ -119,6 +118,10 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
|||
}
|
||||
}, [open]);
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title='添加 Wiki 站管理员'
|
||||
|
|
@ -209,22 +212,33 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
|||
fullWidth
|
||||
sx={{ height: 52 }}
|
||||
value={perm}
|
||||
MenuProps={{
|
||||
sx: {
|
||||
'.Mui-disabled': {
|
||||
opacity: 1,
|
||||
color: 'text.disabled',
|
||||
},
|
||||
},
|
||||
}}
|
||||
onChange={e => setPerm(e.target.value as V1KBUserInviteReq['perm'])}
|
||||
>
|
||||
<MenuItem value={ConstsUserKBPermission.UserKBPermissionFullControl}>
|
||||
完全控制
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||
disabled={!isPro}
|
||||
>
|
||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
||||
文档管理{' '}
|
||||
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||
disabled={!isPro}
|
||||
>
|
||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
||||
数据运营{' '}
|
||||
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { getApiProV1Prompt, postApiProV1Prompt } from '@/request/pro/Prompt';
|
||||
import { DomainKnowledgeBaseDetail } from '@/request/types';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { Box, Slider, TextField } from '@mui/material';
|
||||
|
|
@ -33,11 +34,12 @@ const CardAI = ({ kb }: CardAIProps) => {
|
|||
});
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return license.edition === 1 || license.edition === 2;
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb.id || !isPro) return;
|
||||
if (!kb.id || !PROFESSION_VERSION_PERMISSION.includes(license.edition!))
|
||||
return;
|
||||
getApiProV1Prompt({ kb_id: kb.id! }).then(res => {
|
||||
setValue('content', res.content || '');
|
||||
});
|
||||
|
|
@ -54,7 +56,7 @@ const CardAI = ({ kb }: CardAIProps) => {
|
|||
<SettingCardItem title='智能问答' isEdit={isEdit} onSubmit={onSubmit}>
|
||||
<FormItem
|
||||
vertical
|
||||
tooltip={!isPro && '联创版和企业版可用'}
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
extra={
|
||||
<Box
|
||||
sx={{
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import { ColumnType } from '@ctzhian/ui/dist/Table';
|
|||
import { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import { SettingCardItem, FormItem } from './Common';
|
||||
|
||||
interface CardAuthProps {
|
||||
|
|
@ -114,7 +116,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
}),
|
||||
value.enabled === '2' &&
|
||||
source_type !== EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword
|
||||
? isPro
|
||||
? isBusiness
|
||||
? postApiProV1AuthSet({
|
||||
kb_id,
|
||||
source_type: value.source_type as ConstsSourceType,
|
||||
|
|
@ -157,25 +159,18 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
});
|
||||
});
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return license.edition === 1 || license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
useEffect(() => {
|
||||
const source_type = isPro
|
||||
const source_type = isBusiness
|
||||
? kb.access_settings?.source_type ||
|
||||
EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword
|
||||
: kb.access_settings?.source_type ===
|
||||
EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub
|
||||
? EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub
|
||||
: EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword;
|
||||
: EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword;
|
||||
setValue('source_type', source_type);
|
||||
sourceTypeRef.current = source_type;
|
||||
}, [kb, isPro]);
|
||||
}, [kb, isBusiness]);
|
||||
|
||||
useEffect(() => {
|
||||
if (kb.access_settings?.simple_auth) {
|
||||
|
|
@ -191,7 +186,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
}, [kb]);
|
||||
|
||||
const getAuth = () => {
|
||||
if (isPro) {
|
||||
if (isBusiness) {
|
||||
getApiProV1AuthGet({
|
||||
kb_id,
|
||||
source_type: source_type as ConstsSourceType,
|
||||
|
|
@ -236,7 +231,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
useEffect(() => {
|
||||
if (!kb_id || enabled !== '2') return;
|
||||
getAuth();
|
||||
}, [kb_id, isPro, source_type, enabled]);
|
||||
}, [kb_id, isBusiness, source_type, enabled]);
|
||||
|
||||
const columns: ColumnType<GithubComChaitinPandaWikiProApiAuthV1AuthItem>[] = [
|
||||
{
|
||||
|
|
@ -875,8 +870,18 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
MenuProps={{
|
||||
sx: {
|
||||
'.Mui-disabled': {
|
||||
opacity: 1,
|
||||
color: 'text.disabled',
|
||||
},
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
sx={{ height: 52 }}
|
||||
sx={{
|
||||
height: 52,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword}
|
||||
|
|
@ -885,44 +890,52 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeDingTalk}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
钉钉登录 {isPro ? '' : tips}
|
||||
钉钉登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeFeishu}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
飞书登录 {isPro ? '' : tips}
|
||||
飞书登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeWeCom}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
企业微信登录 {isPro ? '' : tips}
|
||||
企业微信登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeOAuth}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
OAuth 登录 {isPro ? '' : tips}
|
||||
OAuth 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeCAS}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
CAS 登录 {isPro ? '' : tips}
|
||||
CAS 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeLDAP}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
LDAP 登录 {isPro ? '' : tips}
|
||||
LDAP 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
GitHub 登录
|
||||
GitHub 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
DomainKnowledgeBaseDetail,
|
||||
} from '@/request/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
|
|
@ -12,7 +12,6 @@ import {
|
|||
RadioGroup,
|
||||
styled,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
|
||||
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
||||
|
|
@ -37,7 +36,7 @@ const DocumentComments = ({
|
|||
data: DomainAppDetailResp;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
|
|
@ -57,8 +56,6 @@ const DocumentComments = ({
|
|||
);
|
||||
}, [data]);
|
||||
|
||||
const isPro = license.edition === 1 || license.edition === 2;
|
||||
|
||||
const onSubmit = handleSubmit(formData => {
|
||||
putApiV1App(
|
||||
{ id: data.id! },
|
||||
|
|
@ -108,7 +105,7 @@ const DocumentComments = ({
|
|||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='评论审核' tooltip={!isPro && '联创版和企业版可用'}>
|
||||
<FormItem label='评论审核' permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='moderation_enable'
|
||||
|
|
@ -116,7 +113,6 @@ const DocumentComments = ({
|
|||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
value={isPro ? field.value : undefined}
|
||||
onChange={e => {
|
||||
setIsEdit(true);
|
||||
field.onChange(+e.target.value as 1 | 0);
|
||||
|
|
@ -124,12 +120,12 @@ const DocumentComments = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={1}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>启用</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={0}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
@ -150,7 +146,7 @@ const AIQuestion = ({
|
|||
refresh: () => void;
|
||||
}) => {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const { kb_id, license } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
is_enabled: true,
|
||||
|
|
@ -159,7 +155,6 @@ const AIQuestion = ({
|
|||
},
|
||||
});
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const isEnterprise = license.edition === 2;
|
||||
|
||||
const onSubmit = handleSubmit(formData => {
|
||||
putApiV1App(
|
||||
|
|
@ -273,7 +268,7 @@ const AIQuestion = ({
|
|||
)}
|
||||
/>{' '}
|
||||
</FormItem>
|
||||
<FormItem label='免责声明' tooltip={!isEnterprise && '企业版可用'}>
|
||||
<FormItem label='免责声明' permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='disclaimer'
|
||||
|
|
@ -282,7 +277,6 @@ const AIQuestion = ({
|
|||
{...field}
|
||||
fullWidth
|
||||
value={field.value || ''}
|
||||
disabled={!isEnterprise}
|
||||
placeholder='请输入免责声明'
|
||||
onChange={e => {
|
||||
setIsEdit(true);
|
||||
|
|
@ -304,7 +298,7 @@ const DocumentContribution = ({
|
|||
refresh: () => void;
|
||||
}) => {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
is_enable: false,
|
||||
|
|
@ -330,7 +324,6 @@ const DocumentContribution = ({
|
|||
});
|
||||
});
|
||||
|
||||
const isPro = license.edition === 1 || license.edition === 2;
|
||||
useEffect(() => {
|
||||
setValue(
|
||||
'is_enable',
|
||||
|
|
@ -340,21 +333,8 @@ const DocumentContribution = ({
|
|||
}, [data]);
|
||||
|
||||
return (
|
||||
<SettingCardItem
|
||||
title={
|
||||
<>
|
||||
文档贡献
|
||||
{!isPro && (
|
||||
<Tooltip title='联创版和企业版可用' placement='top' arrow>
|
||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
isEdit={isEdit}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<FormItem label='文档贡献'>
|
||||
<SettingCardItem title='文档贡献' isEdit={isEdit} onSubmit={onSubmit}>
|
||||
<FormItem label='文档贡献' permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='is_enable'
|
||||
|
|
@ -362,7 +342,7 @@ const DocumentContribution = ({
|
|||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
value={isPro ? field.value : undefined}
|
||||
value={field.value}
|
||||
onChange={e => {
|
||||
setIsEdit(true);
|
||||
field.onChange(e.target.value === 'true');
|
||||
|
|
@ -370,12 +350,12 @@ const DocumentContribution = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={true}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>启用</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={false}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ import { Controller, useForm } from 'react-hook-form';
|
|||
import { useDispatch } from 'react-redux';
|
||||
import AddRole from './AddRole';
|
||||
import { Form, FormItem, SettingCardItem } from './Common';
|
||||
import {
|
||||
PROFESSION_VERSION_PERMISSION,
|
||||
BUSINESS_VERSION_PERMISSION,
|
||||
} from '@/constant/version';
|
||||
|
||||
type ApiTokenPermission =
|
||||
GithubComChaitinPandaWikiProApiTokenV1CreateAPITokenReq['permission'];
|
||||
|
|
@ -69,8 +73,8 @@ const ApiToken = () => {
|
|||
perm: ConstsUserKBPermission.UserKBPermissionFullControl,
|
||||
},
|
||||
});
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const onDeleteApiToken = (id: string, name: string) => {
|
||||
|
|
@ -131,9 +135,9 @@ const ApiToken = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb_id) return;
|
||||
if (!kb_id || !isBusiness) return;
|
||||
getApiTokenList();
|
||||
}, [kb_id]);
|
||||
}, [kb_id, isBusiness]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!addOpen) reset();
|
||||
|
|
@ -142,27 +146,17 @@ const ApiToken = () => {
|
|||
return (
|
||||
<SettingCardItem
|
||||
title='API Token'
|
||||
permission={BUSINESS_VERSION_PERMISSION}
|
||||
extra={
|
||||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<Button
|
||||
color='primary'
|
||||
size='small'
|
||||
disabled={!isEnterprise}
|
||||
onClick={() => setAddOpen(true)}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
创建 API Token
|
||||
</Button>
|
||||
|
||||
<Tooltip title={'企业版可用'} placement='top' arrow>
|
||||
<InfoIcon
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
fontSize: 14,
|
||||
display: !isEnterprise ? 'block' : 'none',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
|
|
@ -232,7 +226,7 @@ const ApiToken = () => {
|
|||
size='small'
|
||||
sx={{ width: 120 }}
|
||||
value={it.permission}
|
||||
disabled={!isEnterprise || user.role !== 'admin'}
|
||||
disabled={!isBusiness || user.role !== 'admin'}
|
||||
onChange={e =>
|
||||
onUpdateApiToken(it.id!, e.target.value as ApiTokenPermission)
|
||||
}
|
||||
|
|
@ -259,7 +253,7 @@ const ApiToken = () => {
|
|||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? '权限不足'
|
||||
: '企业版可用'
|
||||
: '商业版可用'
|
||||
}
|
||||
placement='top'
|
||||
arrow
|
||||
|
|
@ -270,7 +264,7 @@ const ApiToken = () => {
|
|||
fontSize: 14,
|
||||
ml: 1,
|
||||
visibility:
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? 'visible'
|
||||
|
|
@ -285,13 +279,13 @@ const ApiToken = () => {
|
|||
type='icon-icon_tool_close'
|
||||
sx={{
|
||||
cursor:
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? 'not-allowed'
|
||||
: 'pointer',
|
||||
color:
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? 'text.disabled'
|
||||
|
|
@ -299,7 +293,7 @@ const ApiToken = () => {
|
|||
}}
|
||||
onClick={() => {
|
||||
if (
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
)
|
||||
|
|
@ -367,17 +361,16 @@ const ApiToken = () => {
|
|||
>
|
||||
完全控制
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||
>
|
||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
||||
文档管理
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||
>
|
||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
||||
数据运营
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
|
|
@ -405,9 +398,9 @@ const CardKB = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
const isPro = useMemo(() => {
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb_id) return;
|
||||
|
|
@ -513,7 +506,7 @@ const CardKB = () => {
|
|||
size='small'
|
||||
sx={{ width: 180 }}
|
||||
value={it.perms}
|
||||
disabled={!isEnterprise || it.role === 'admin'}
|
||||
disabled={!isPro || it.role === 'admin'}
|
||||
onChange={e =>
|
||||
onUpdateUserPermission(
|
||||
it.id!,
|
||||
|
|
@ -542,7 +535,7 @@ const CardKB = () => {
|
|||
title={
|
||||
it.role === 'admin'
|
||||
? '超级管理员不可被修改权限'
|
||||
: '企业版可用'
|
||||
: '专业版可用'
|
||||
}
|
||||
placement='top'
|
||||
arrow
|
||||
|
|
@ -553,9 +546,7 @@ const CardKB = () => {
|
|||
fontSize: 14,
|
||||
ml: 1,
|
||||
visibility:
|
||||
!isEnterprise || it.role === 'admin'
|
||||
? 'visible'
|
||||
: 'hidden',
|
||||
!isPro || it.role === 'admin' ? 'visible' : 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { DomainKnowledgeBaseDetail } from '@/request/types';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Link,
|
||||
|
|
@ -13,10 +12,11 @@ import {
|
|||
import ShowText from '@/components/ShowText';
|
||||
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormItem, SettingCardItem } from './Common';
|
||||
import { DomainAppDetailResp } from '@/request/types';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
const CardRobotApi = ({
|
||||
|
|
@ -29,11 +29,6 @@ const CardRobotApi = ({
|
|||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
|
|
@ -114,10 +109,7 @@ const CardRobotApi = ({
|
|||
}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<FormItem
|
||||
label='问答机器人 API'
|
||||
tooltip={!isEnterprise ? '企业版可用' : undefined}
|
||||
>
|
||||
<FormItem label='问答机器人 API' permission={BUSINESS_VERSION_PERMISSION}>
|
||||
<FormControl>
|
||||
<Controller
|
||||
control={control}
|
||||
|
|
@ -133,13 +125,11 @@ const CardRobotApi = ({
|
|||
<Stack direction={'row'}>
|
||||
<FormControlLabel
|
||||
value={true}
|
||||
disabled={!isEnterprise}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>启用</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={false}
|
||||
disabled={!isEnterprise}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>禁用</Box>}
|
||||
/>
|
||||
|
|
@ -150,7 +140,7 @@ const CardRobotApi = ({
|
|||
</FormControl>
|
||||
</FormItem>
|
||||
|
||||
{isEnabled && (
|
||||
{isEnabled && BUSINESS_VERSION_PERMISSION.includes(license.edition!) && (
|
||||
<>
|
||||
<FormItem label='API Token' required>
|
||||
<Controller
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import {
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { FormItem, SettingCardItem } from './Common';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
const CardRobotWecomService = ({
|
||||
kb,
|
||||
|
|
@ -262,38 +264,40 @@ const CardRobotWecomService = ({
|
|||
<Icon type='icon-jinggao' sx={{ fontSize: 18 }} />
|
||||
人工客服转接配置:当用户触发以下场景时,会自动转接人工客服
|
||||
</Stack>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
包含特定
|
||||
<VersionMask permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
包含特定
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...containKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
完全匹配
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...containKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
完全匹配
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...equalKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...equalKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
</VersionMask>
|
||||
</>
|
||||
)}
|
||||
</SettingCardItem>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from '@/request/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
|
|
@ -17,7 +18,7 @@ import {
|
|||
TextField,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { FormItem, SettingCardItem } from './Common';
|
||||
|
||||
|
|
@ -32,15 +33,9 @@ const WatermarkForm = ({
|
|||
data?: DomainAppDetailResp;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const [watermarkIsEdit, setWatermarkIsEdit] = useState(false);
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
const { control, handleSubmit, setValue, watch } = useForm({
|
||||
defaultValues: {
|
||||
watermark_setting: data?.settings?.watermark_setting ?? null,
|
||||
watermark_content: data?.settings?.watermark_content ?? '',
|
||||
|
|
@ -48,9 +43,6 @@ const WatermarkForm = ({
|
|||
});
|
||||
|
||||
const watermarkSetting = watch('watermark_setting');
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const handleSaveWatermark = handleSubmit(values => {
|
||||
if (!data?.id || values.watermark_setting === null) return;
|
||||
|
|
@ -82,8 +74,9 @@ const WatermarkForm = ({
|
|||
title='水印'
|
||||
isEdit={watermarkIsEdit}
|
||||
onSubmit={handleSaveWatermark}
|
||||
permission={BUSINESS_VERSION_PERMISSION}
|
||||
>
|
||||
<FormItem label='水印开关' tooltip={!isEnterprise && '企业版可用'}>
|
||||
<FormItem label='水印开关'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='watermark_setting'
|
||||
|
|
@ -98,18 +91,18 @@ const WatermarkForm = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={ConstsWatermarkSetting.WatermarkVisible}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>显性水印</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={ConstsWatermarkSetting.WatermarkHidden}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>隐形水印</StyledRadioLabel>}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
value={ConstsWatermarkSetting.WatermarkDisabled}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
@ -128,7 +121,6 @@ const WatermarkForm = ({
|
|||
placeholder='请输入水印内容, 支持多行输入'
|
||||
multiline
|
||||
minRows={2}
|
||||
disabled={!isEnterprise}
|
||||
onChange={e => {
|
||||
setWatermarkIsEdit(true);
|
||||
field.onChange(e.target.value);
|
||||
|
|
@ -146,9 +138,6 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
|||
const { license } = useAppSelector(state => state.config);
|
||||
const [questionInputValue, setQuestionInputValue] = useState('');
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
|
|
@ -169,17 +158,18 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb.id || !isEnterprise) return;
|
||||
if (!kb.id || !BUSINESS_VERSION_PERMISSION.includes(license.edition!))
|
||||
return;
|
||||
getApiProV1Block({ kb_id: kb.id! }).then(res => {
|
||||
setValue('block_words', res.words || []);
|
||||
});
|
||||
}, [kb, isEnterprise]);
|
||||
}, [kb, license.edition]);
|
||||
|
||||
return (
|
||||
<SettingCardItem title='内容合规' isEdit={isEdit} onSubmit={onSubmit}>
|
||||
<FormItem
|
||||
vertical
|
||||
tooltip={!isEnterprise && '企业版可用'}
|
||||
permission={BUSINESS_VERSION_PERMISSION}
|
||||
label='屏蔽 AI 问答中的关键字'
|
||||
>
|
||||
<Controller
|
||||
|
|
@ -193,7 +183,6 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
|||
inputValue={questionInputValue}
|
||||
options={[]}
|
||||
fullWidth
|
||||
disabled={!isEnterprise}
|
||||
onInputChange={(_, value) => {
|
||||
setQuestionInputValue(value);
|
||||
}}
|
||||
|
|
@ -234,23 +223,14 @@ const CopyForm = ({
|
|||
data?: DomainAppDetailResp;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
copy_setting: data?.settings?.copy_setting ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const handleSaveWatermark = handleSubmit(values => {
|
||||
if (!data?.id || values.copy_setting === null) return;
|
||||
putApiV1App(
|
||||
|
|
@ -280,7 +260,7 @@ const CopyForm = ({
|
|||
isEdit={isEdit}
|
||||
onSubmit={handleSaveWatermark}
|
||||
>
|
||||
<FormItem label='限制复制' tooltip={!isEnterprise && '企业版可用'}>
|
||||
<FormItem label='限制复制' permission={BUSINESS_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='copy_setting'
|
||||
|
|
@ -295,18 +275,18 @@ const CopyForm = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={ConstsCopySetting.CopySettingNone}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>不做限制</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={ConstsCopySetting.CopySettingAppend}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>增加内容尾巴</StyledRadioLabel>}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
value={ConstsCopySetting.CopySettingDisabled}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁止复制内容</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
|||
defaultValues: {
|
||||
desc: '',
|
||||
keyword: '',
|
||||
auto_sitemap: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -44,7 +43,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
|||
useEffect(() => {
|
||||
setValue('desc', data.settings?.desc || '');
|
||||
setValue('keyword', data.settings?.keyword || '');
|
||||
setValue('auto_sitemap', data.settings?.auto_sitemap ?? false);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
|
|
@ -88,25 +86,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
|||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label='自动生成 Sitemap'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='auto_sitemap'
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
size='small'
|
||||
sx={{ p: 0, m: 0 }}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
</SettingCardItem>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import Card from '@/components/Card';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { Button, Stack, styled, SxProps, Tooltip } from '@mui/material';
|
||||
import { createContext, useContext } from 'react';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
|
||||
const StyledForm = styled('form')<{ gap?: number | string }>(
|
||||
({ theme, gap = 2 }) => ({
|
||||
|
|
@ -40,6 +42,7 @@ const StyledFormLabel = styled('span')<{ required?: boolean }>(
|
|||
|
||||
export const StyledFormItem = styled('div')<{ vertical?: boolean }>(
|
||||
({ theme, vertical }) => ({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: vertical ? 'flex-start' : 'center',
|
||||
flexDirection: vertical ? 'column' : 'row',
|
||||
|
|
@ -82,6 +85,7 @@ export const FormItem = ({
|
|||
extra,
|
||||
sx,
|
||||
labelSx,
|
||||
permission,
|
||||
}: {
|
||||
label?: string | React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
|
|
@ -92,31 +96,37 @@ export const FormItem = ({
|
|||
extra?: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
labelSx?: SxProps;
|
||||
permission?: number[];
|
||||
}) => {
|
||||
const { vertical: verticalContext, labelWidth: labelWidthContext } =
|
||||
useContext(FormContext);
|
||||
return (
|
||||
<StyledFormItem vertical={vertical || verticalContext} sx={sx}>
|
||||
<StyledFormLabelWrapper
|
||||
vertical={vertical || verticalContext}
|
||||
labelWidth={labelWidth || labelWidthContext}
|
||||
sx={labelSx}
|
||||
>
|
||||
<Stack direction='row' alignItems='center' flex={1}>
|
||||
<StyledFormLabel required={required}>{label}</StyledFormLabel>
|
||||
{tooltip && typeof tooltip === 'string' ? (
|
||||
<Tooltip title={tooltip} placement='top' arrow>
|
||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
tooltip
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{extra}
|
||||
</StyledFormLabelWrapper>
|
||||
{children}
|
||||
</StyledFormItem>
|
||||
return (
|
||||
<VersionMask permission={permission}>
|
||||
<StyledFormItem vertical={vertical || verticalContext} sx={sx}>
|
||||
<StyledFormLabelWrapper
|
||||
vertical={vertical || verticalContext}
|
||||
labelWidth={labelWidth || labelWidthContext}
|
||||
sx={labelSx}
|
||||
>
|
||||
<Stack direction='row' alignItems='center' flex={1}>
|
||||
<StyledFormLabel required={required}>{label}</StyledFormLabel>
|
||||
{tooltip && typeof tooltip === 'string' ? (
|
||||
<Tooltip title={tooltip} placement='top' arrow>
|
||||
<InfoIcon
|
||||
sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
tooltip
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{extra}
|
||||
</StyledFormLabelWrapper>
|
||||
{children}
|
||||
</StyledFormItem>
|
||||
</VersionMask>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -142,6 +152,7 @@ export const SettingCard = ({
|
|||
};
|
||||
|
||||
const StyledSettingCardItem = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
'&:not(:last-child)': {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
paddingBottom: theme.spacing(4),
|
||||
|
|
@ -204,6 +215,12 @@ export const SettingCardItem = ({
|
|||
extra,
|
||||
more,
|
||||
sx,
|
||||
permission = [
|
||||
ConstsLicenseEdition.LicenseEditionFree,
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
],
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
|
|
@ -212,6 +229,7 @@ export const SettingCardItem = ({
|
|||
extra?: React.ReactNode;
|
||||
more?: SettingCardItemMore;
|
||||
sx?: SxProps;
|
||||
permission?: number[];
|
||||
}) => {
|
||||
const renderMore = (more: SettingCardItemMore) => {
|
||||
if (more && typeof more === 'object' && 'type' in more) {
|
||||
|
|
@ -237,20 +255,23 @@ export const SettingCardItem = ({
|
|||
return more;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledSettingCardItem sx={sx}>
|
||||
<StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemTitle>
|
||||
{title} {renderMore(more)}
|
||||
</StyledSettingCardItemTitle>
|
||||
{isEdit && (
|
||||
<Button variant='contained' size='small' onClick={onSubmit}>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
{extra}
|
||||
</StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemContent>{children}</StyledSettingCardItemContent>
|
||||
</StyledSettingCardItem>
|
||||
<VersionMask permission={permission}>
|
||||
<StyledSettingCardItem sx={sx}>
|
||||
<StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemTitle>
|
||||
{title} {renderMore(more)}
|
||||
</StyledSettingCardItemTitle>
|
||||
{isEdit && (
|
||||
<Button variant='contained' size='small' onClick={onSubmit}>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
{extra}
|
||||
</StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemContent>{children}</StyledSettingCardItemContent>
|
||||
</StyledSettingCardItem>
|
||||
</VersionMask>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SettingCardItem } from '../Common';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { Modal, message } from '@ctzhian/ui';
|
||||
import { Stack, Button } from '@mui/material';
|
||||
import { Box } from '@mui/material';
|
||||
import { postApiProV1AuthGroupSync } from '@/request/pro/AuthOrg';
|
||||
import {
|
||||
|
|
@ -19,6 +16,7 @@ import {
|
|||
deleteApiProV1AuthGroupDelete,
|
||||
} from '@/request/pro/AuthGroup';
|
||||
import GroupTree from './GroupTree';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface UserGroupProps {
|
||||
enabled: string;
|
||||
|
|
@ -45,10 +43,6 @@ const UserGroup = ({
|
|||
GithubComChaitinPandaWikiProApiAuthV1AuthGroupTreeItem[]
|
||||
>([]);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const onDeleteUserGroup = (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '删除用户组',
|
||||
|
|
@ -74,10 +68,15 @@ const UserGroup = ({
|
|||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!kb_id || enabled !== '2' || !isEnterprise) return;
|
||||
if (
|
||||
!kb_id ||
|
||||
enabled !== '2' ||
|
||||
!BUSINESS_VERSION_PERMISSION.includes(license.edition!)
|
||||
)
|
||||
return;
|
||||
getUserGroup();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [kb_id, enabled, isEnterprise]);
|
||||
}, [kb_id, enabled, license.edition!]);
|
||||
|
||||
const handleMove = async ({
|
||||
id,
|
||||
|
|
@ -123,32 +122,7 @@ const UserGroup = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<SettingCardItem
|
||||
title='用户组'
|
||||
more={
|
||||
!isEnterprise && (
|
||||
<Tooltip title='企业版可用' placement='top' arrow>
|
||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
// extra={
|
||||
// isEnterprise &&
|
||||
// [
|
||||
// ConstsSourceType.SourceTypeWeCom,
|
||||
// ConstsSourceType.SourceTypeDingTalk,
|
||||
// ].includes(sourceType as ConstsSourceType) && (
|
||||
// <Button
|
||||
// color='primary'
|
||||
// size='small'
|
||||
// onClick={handleSync}
|
||||
// loading={syncLoading}
|
||||
// >
|
||||
// 同步组织架构和成员
|
||||
// </Button>
|
||||
// )
|
||||
// }
|
||||
>
|
||||
<SettingCardItem title='用户组' permission={BUSINESS_VERSION_PERMISSION}>
|
||||
<Box
|
||||
sx={{
|
||||
border: '1px dashed',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ import QAReferer from './QAReferer';
|
|||
import RTVisitor from './RTVisitor';
|
||||
import TypeCount from './TypeCount';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import {
|
||||
BUSINESS_VERSION_PERMISSION,
|
||||
PROFESSION_VERSION_PERMISSION,
|
||||
} from '@/constant/version';
|
||||
|
||||
export const TimeList = [
|
||||
{ label: '近 24 小时', value: 1 },
|
||||
|
|
@ -25,13 +30,40 @@ const Statistic = () => {
|
|||
const isWideScreen = useMediaQuery('(min-width:1190px)');
|
||||
|
||||
const timeList = useMemo(() => {
|
||||
const isPro = license.edition === 1 || license.edition === 2;
|
||||
const isEnterprise = license.edition === 2;
|
||||
const isPro = PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
const isBusiness = BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
return [
|
||||
{ label: '近 24 小时', value: 1, disabled: false },
|
||||
{ label: '近 7 天', value: 7, disabled: !isPro },
|
||||
{ label: '近 30 天', value: 30, disabled: !isEnterprise },
|
||||
{ label: '近 90 天', value: 90, disabled: !isEnterprise },
|
||||
{
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||
<span>近 7 天</span>
|
||||
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: 7,
|
||||
disabled: !isPro,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||
<span>近 30 天</span>
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: 30,
|
||||
disabled: !isBusiness,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||
<span>近 90 天</span>
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: 90,
|
||||
disabled: !isBusiness,
|
||||
},
|
||||
];
|
||||
}, [license]);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import {
|
|||
GetApiV1NodeListParams,
|
||||
GetApiV1NodeRecommendNodesParams,
|
||||
V1NodeDetailResp,
|
||||
V1NodeRestudyReq,
|
||||
V1NodeRestudyResp,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
|
|
@ -263,6 +265,38 @@ export const getApiV1NodeRecommendNodes = (
|
|||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 文档重新学习
|
||||
*
|
||||
* @tags Node
|
||||
* @name PostApiV1NodeRestudy
|
||||
* @summary 文档重新学习
|
||||
* @request POST:/api/v1/node/restudy
|
||||
* @secure
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: V1NodeRestudyResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postApiV1NodeRestudy = (
|
||||
param: V1NodeRestudyReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: V1NodeRestudyResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/node/restudy`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Summary Node
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import httpRequest, { ContentType, RequestParams } from "./httpClient";
|
||||
import { DomainResponse, V1NodeRestudyReq, V1NodeRestudyResp } from "./types";
|
||||
|
||||
/**
|
||||
* @description 文档重新学习
|
||||
*
|
||||
* @tags NodeRestudy
|
||||
* @name PostApiV1NodeRestudy
|
||||
* @summary 文档重新学习
|
||||
* @request POST:/api/v1/node/restudy
|
||||
* @secure
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: V1NodeRestudyResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postApiV1NodeRestudy = (
|
||||
param: V1NodeRestudyReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: V1NodeRestudyResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/node/restudy`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
|
@ -10,7 +10,6 @@ export * from './Message'
|
|||
export * from './Model'
|
||||
export * from './Node'
|
||||
export * from './NodePermission'
|
||||
export * from './NodeRestudy'
|
||||
export * from './Stat'
|
||||
export * from './User'
|
||||
export * from './types'
|
||||
|
|
|
|||
|
|
@ -52,10 +52,12 @@ export enum ConstsSourceType {
|
|||
export enum ConstsLicenseEdition {
|
||||
/** 开源版 */
|
||||
LicenseEditionFree = 0,
|
||||
/** 联创版 */
|
||||
LicenseEditionContributor = 1,
|
||||
/** 专业版 */
|
||||
LicenseEditionProfession = 1,
|
||||
/** 企业版 */
|
||||
LicenseEditionEnterprise = 2,
|
||||
/** 商业版 */
|
||||
LicenseEditionBusiness = 3,
|
||||
}
|
||||
|
||||
export enum ConstsContributeType {
|
||||
|
|
@ -465,6 +467,7 @@ export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp {
|
|||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthWecomReq {
|
||||
is_app?: boolean;
|
||||
kb_id?: string;
|
||||
redirect_url?: string;
|
||||
}
|
||||
|
|
@ -668,8 +671,6 @@ export interface GetApiProV1TokenListParams {
|
|||
}
|
||||
|
||||
export interface PostApiV1LicensePayload {
|
||||
/** license edition */
|
||||
license_edition: "contributor" | "enterprise";
|
||||
/** license type */
|
||||
license_type: "file" | "code";
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -175,10 +175,12 @@ export enum ConstsNodeAccessPerm {
|
|||
export enum ConstsLicenseEdition {
|
||||
/** 开源版 */
|
||||
LicenseEditionFree = 0,
|
||||
/** 联创版 */
|
||||
LicenseEditionContributor = 1,
|
||||
/** 专业版 */
|
||||
LicenseEditionProfession = 1,
|
||||
/** 企业版 */
|
||||
LicenseEditionEnterprise = 2,
|
||||
/** 商业版 */
|
||||
LicenseEditionBusiness = 3,
|
||||
}
|
||||
|
||||
export enum ConstsHomePageSetting {
|
||||
|
|
@ -1645,8 +1647,9 @@ export interface V1NodePermissionResp {
|
|||
}
|
||||
|
||||
export interface V1NodeRestudyReq {
|
||||
kb_id?: string;
|
||||
node_ids?: string[];
|
||||
kb_id: string;
|
||||
/** @minItems 1 */
|
||||
node_ids: string[];
|
||||
}
|
||||
|
||||
export type V1NodeRestudyResp = Record<string, any>;
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ export interface ConversationItem {
|
|||
message_id: string;
|
||||
source: 'history' | 'chat';
|
||||
chunk_result: ChunkResultItem[];
|
||||
result_expend: boolean;
|
||||
thinking_expend: boolean;
|
||||
thinking_content: string;
|
||||
id: string;
|
||||
}
|
||||
|
|
@ -382,6 +384,8 @@ const AiQaContent: React.FC<{
|
|||
const solution = await cap.solve();
|
||||
token = solution.token;
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setThinking(4);
|
||||
message.error('验证失败');
|
||||
console.log(error, 'error---------');
|
||||
return;
|
||||
|
|
@ -465,6 +469,8 @@ const AiQaContent: React.FC<{
|
|||
if (lastConversation) {
|
||||
lastConversation.a = answerContent;
|
||||
lastConversation.thinking_content = thinkingContent;
|
||||
lastConversation.result_expend = false;
|
||||
lastConversation.thinking_expend = false;
|
||||
}
|
||||
return newConversation;
|
||||
});
|
||||
|
|
@ -513,6 +519,8 @@ const AiQaContent: React.FC<{
|
|||
source: 'chat',
|
||||
chunk_result: [],
|
||||
thinking_content: '',
|
||||
result_expend: true,
|
||||
thinking_expend: true,
|
||||
id: uuidv4(),
|
||||
});
|
||||
messageIdRef.current = '';
|
||||
|
|
@ -631,6 +639,8 @@ const AiQaContent: React.FC<{
|
|||
source: 'history',
|
||||
chunk_result: [],
|
||||
thinking_content: '',
|
||||
result_expend: true,
|
||||
thinking_expend: true,
|
||||
id: uuidv4(),
|
||||
});
|
||||
}
|
||||
|
|
@ -667,6 +677,8 @@ const AiQaContent: React.FC<{
|
|||
chunk_result: [],
|
||||
thinking_content: '',
|
||||
id: uuidv4(),
|
||||
result_expend: true,
|
||||
thinking_expend: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -791,7 +803,16 @@ const AiQaContent: React.FC<{
|
|||
<StyledAiBubble>
|
||||
{/* 搜索结果 */}
|
||||
{item.chunk_result.length > 0 && (
|
||||
<StyledChunkAccordion defaultExpanded>
|
||||
<StyledChunkAccordion
|
||||
expanded={item.result_expend}
|
||||
onChange={(event, expanded) => {
|
||||
setConversation(prev => {
|
||||
const newConversation = [...prev];
|
||||
newConversation[index].result_expend = expanded;
|
||||
return newConversation;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<StyledChunkAccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
|
|
@ -837,7 +858,16 @@ const AiQaContent: React.FC<{
|
|||
|
||||
{/* 思考过程 */}
|
||||
{!!item.thinking_content && (
|
||||
<StyledThinkingAccordion defaultExpanded>
|
||||
<StyledThinkingAccordion
|
||||
expanded={item.thinking_expend}
|
||||
onChange={(event, expanded) => {
|
||||
setConversation(prev => {
|
||||
const newConversation = [...prev];
|
||||
newConversation[index].thinking_expend = expanded;
|
||||
return newConversation;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<StyledThinkingAccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
|
|
@ -929,6 +959,9 @@ const AiQaContent: React.FC<{
|
|||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<Box>
|
||||
{kbDetail?.settings?.disclaimer_settings?.content}
|
||||
</Box>
|
||||
</StyledActionStack>
|
||||
)}
|
||||
</StyledAiBubble>
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ export const StyledUserBubble = styled(Box)(({ theme }) => ({
|
|||
|
||||
export const StyledAiBubble = styled(Box)(({ theme }) => ({
|
||||
alignSelf: 'flex-start',
|
||||
maxWidth: '85%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: theme.spacing(3),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
import { useState, useCallback, useRef } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import { ConversationItem } from '../types';
|
||||
import { ChunkResultItem } from '@/assets/type';
|
||||
|
||||
export const useConversation = () => {
|
||||
const [conversation, setConversation] = useState<ConversationItem[]>([]);
|
||||
const [fullAnswer, setFullAnswer] = useState<string>('');
|
||||
const [chunkResult, setChunkResult] = useState<ChunkResultItem[]>([]);
|
||||
const [thinkingContent, setThinkingContent] = useState<string>('');
|
||||
const [answer, setAnswer] = useState('');
|
||||
const [isChunkResult, setIsChunkResult] = useState(false);
|
||||
const [isThinking, setIsThinking] = useState(false);
|
||||
|
||||
const messageIdRef = useRef('');
|
||||
|
||||
const addQuestion = useCallback(
|
||||
(q: string, reset: boolean = false) => {
|
||||
const newConversation = reset
|
||||
? []
|
||||
: conversation.some(item => item.source === 'history')
|
||||
? []
|
||||
: [...conversation];
|
||||
|
||||
newConversation.push({
|
||||
q,
|
||||
a: '',
|
||||
score: 0,
|
||||
message_id: '',
|
||||
update_time: '',
|
||||
source: 'chat',
|
||||
chunk_result: [],
|
||||
thinking_content: '',
|
||||
});
|
||||
|
||||
messageIdRef.current = '';
|
||||
setConversation(newConversation);
|
||||
setChunkResult([]);
|
||||
setThinkingContent('');
|
||||
setAnswer('');
|
||||
setFullAnswer('');
|
||||
},
|
||||
[conversation],
|
||||
);
|
||||
|
||||
const updateLastConversation = useCallback(() => {
|
||||
setAnswer(prevAnswer => {
|
||||
setThinkingContent(prevThinkingContent => {
|
||||
setChunkResult(prevChunkResult => {
|
||||
setConversation(prev => {
|
||||
const newConversation = [...prev];
|
||||
const lastConversation =
|
||||
newConversation[newConversation.length - 1];
|
||||
if (lastConversation) {
|
||||
lastConversation.a = prevAnswer;
|
||||
lastConversation.update_time = dayjs().format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
);
|
||||
lastConversation.message_id = messageIdRef.current;
|
||||
lastConversation.source = 'chat';
|
||||
lastConversation.chunk_result = prevChunkResult;
|
||||
lastConversation.thinking_content = prevThinkingContent;
|
||||
}
|
||||
return newConversation;
|
||||
});
|
||||
return prevChunkResult;
|
||||
});
|
||||
return prevThinkingContent;
|
||||
});
|
||||
return '';
|
||||
});
|
||||
|
||||
setFullAnswer('');
|
||||
}, []);
|
||||
|
||||
const updateConversationScore = useCallback(
|
||||
(message_id: string, score: number) => {
|
||||
setConversation(prev =>
|
||||
prev.map(item =>
|
||||
item.message_id === message_id ? { ...item, score } : item,
|
||||
),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const resetConversation = useCallback(() => {
|
||||
setConversation([]);
|
||||
setChunkResult([]);
|
||||
setAnswer('');
|
||||
setFullAnswer('');
|
||||
setThinkingContent('');
|
||||
messageIdRef.current = '';
|
||||
}, []);
|
||||
|
||||
return {
|
||||
conversation,
|
||||
setConversation,
|
||||
fullAnswer,
|
||||
setFullAnswer,
|
||||
chunkResult,
|
||||
setChunkResult,
|
||||
thinkingContent,
|
||||
setThinkingContent,
|
||||
answer,
|
||||
setAnswer,
|
||||
isChunkResult,
|
||||
setIsChunkResult,
|
||||
isThinking,
|
||||
setIsThinking,
|
||||
messageIdRef,
|
||||
addQuestion,
|
||||
updateLastConversation,
|
||||
updateConversationScore,
|
||||
resetConversation,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
import { useState, useRef, useCallback } from 'react';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { UploadedImage } from '../types';
|
||||
import { MAX_IMAGES, MAX_IMAGE_SIZE } from '../constants';
|
||||
|
||||
export const useImageUpload = () => {
|
||||
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const cleanupImageUrls = useCallback((images: UploadedImage[]) => {
|
||||
images.forEach(img => {
|
||||
if (img.url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(img.url);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleImageSelect = useCallback(
|
||||
async (files: FileList | null) => {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const remainingSlots = MAX_IMAGES - uploadedImages.length;
|
||||
if (remainingSlots <= 0) {
|
||||
message.warning(`最多只能上传 ${MAX_IMAGES} 张图片`);
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToAdd = Array.from(files).slice(0, remainingSlots);
|
||||
const newImages: UploadedImage[] = [];
|
||||
|
||||
for (const file of filesToAdd) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
message.error('只支持上传图片文件');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.size > MAX_IMAGE_SIZE) {
|
||||
message.error('图片大小不能超过 10MB');
|
||||
continue;
|
||||
}
|
||||
|
||||
const localUrl = URL.createObjectURL(file);
|
||||
newImages.push({
|
||||
id: Date.now().toString() + Math.random(),
|
||||
url: localUrl,
|
||||
file,
|
||||
});
|
||||
}
|
||||
|
||||
setUploadedImages(prev => [...prev, ...newImages]);
|
||||
},
|
||||
[uploadedImages.length],
|
||||
);
|
||||
|
||||
const handleImageUpload = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
handleImageSelect(event.target.files);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
},
|
||||
[handleImageSelect],
|
||||
);
|
||||
|
||||
const handleRemoveImage = useCallback((id: string) => {
|
||||
setUploadedImages(prev => {
|
||||
const imageToRemove = prev.find(img => img.id === id);
|
||||
if (imageToRemove && imageToRemove.url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(imageToRemove.url);
|
||||
}
|
||||
return prev.filter(img => img.id !== id);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handlePaste = useCallback(
|
||||
async (e: React.ClipboardEvent<HTMLDivElement>) => {
|
||||
const items = e.clipboardData?.items;
|
||||
if (!items) return;
|
||||
|
||||
const imageFiles: File[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.type.startsWith('image/')) {
|
||||
const file = item.getAsFile();
|
||||
if (file) imageFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
e.preventDefault();
|
||||
const dataTransfer = new DataTransfer();
|
||||
imageFiles.forEach(file => dataTransfer.items.add(file));
|
||||
await handleImageSelect(dataTransfer.files);
|
||||
}
|
||||
},
|
||||
[handleImageSelect],
|
||||
);
|
||||
|
||||
const clearImages = useCallback(() => {
|
||||
cleanupImageUrls(uploadedImages);
|
||||
setUploadedImages([]);
|
||||
}, [uploadedImages, cleanupImageUrls]);
|
||||
|
||||
return {
|
||||
uploadedImages,
|
||||
fileInputRef,
|
||||
handleImageUpload,
|
||||
handleRemoveImage,
|
||||
handlePaste,
|
||||
clearImages,
|
||||
};
|
||||
};
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import SSEClient from '@/utils/fetch';
|
||||
import { handleThinkingContent } from '../utils';
|
||||
import { SSEMessageData, ChatRequestData } from '../types';
|
||||
import { AnswerStatusType, CAP_CONFIG, SSE_CONFIG } from '../constants';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface UseSSEChatProps {
|
||||
conversationId: string;
|
||||
setConversationId: React.Dispatch<React.SetStateAction<string>>;
|
||||
nonce: string;
|
||||
setNonce: React.Dispatch<React.SetStateAction<string>>;
|
||||
messageIdRef: React.MutableRefObject<string>;
|
||||
setFullAnswer: React.Dispatch<React.SetStateAction<string>>;
|
||||
setAnswer: React.Dispatch<React.SetStateAction<string>>;
|
||||
setThinkingContent: React.Dispatch<React.SetStateAction<string>>;
|
||||
setChunkResult: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
setConversation: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
setIsChunkResult: (value: boolean) => void;
|
||||
setIsThinking: (value: boolean) => void;
|
||||
setThinking: (value: AnswerStatusType) => void;
|
||||
setLoading: (value: boolean) => void;
|
||||
scrollToBottom: () => void;
|
||||
}
|
||||
|
||||
export const useSSEChat = ({
|
||||
conversationId,
|
||||
setConversationId,
|
||||
nonce,
|
||||
setNonce,
|
||||
messageIdRef,
|
||||
setFullAnswer,
|
||||
setAnswer,
|
||||
setThinkingContent,
|
||||
setChunkResult,
|
||||
setConversation,
|
||||
setIsChunkResult,
|
||||
setIsThinking,
|
||||
setThinking,
|
||||
setLoading,
|
||||
scrollToBottom,
|
||||
}: UseSSEChatProps) => {
|
||||
const sseClientRef = useRef<SSEClient<SSEMessageData> | null>(null);
|
||||
|
||||
const initializeSSE = useCallback(() => {
|
||||
sseClientRef.current = new SSEClient({
|
||||
url: SSE_CONFIG.url,
|
||||
headers: SSE_CONFIG.headers,
|
||||
onCancel: () => {
|
||||
setLoading(false);
|
||||
setThinking(4);
|
||||
setAnswer(prev => {
|
||||
let value = '';
|
||||
if (prev) {
|
||||
value = prev + '\n\n<error>Request canceled</error>';
|
||||
}
|
||||
setConversation(prev => {
|
||||
const newConversation = [...prev];
|
||||
if (newConversation[newConversation.length - 1]) {
|
||||
newConversation[newConversation.length - 1].a = value;
|
||||
newConversation[newConversation.length - 1].update_time =
|
||||
dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
newConversation[newConversation.length - 1].message_id =
|
||||
messageIdRef.current;
|
||||
}
|
||||
return newConversation;
|
||||
});
|
||||
return '';
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [messageIdRef, setAnswer, setConversation, setLoading, setThinking]);
|
||||
|
||||
const chatAnswer = useCallback(
|
||||
async (q: string) => {
|
||||
setLoading(true);
|
||||
setThinking(1);
|
||||
|
||||
let token = '';
|
||||
try {
|
||||
const Cap = (await import('@cap.js/widget')).default;
|
||||
const cap = new Cap({ apiEndpoint: CAP_CONFIG.apiEndpoint });
|
||||
const solution = await cap.solve();
|
||||
token = solution.token;
|
||||
} catch (error) {
|
||||
message.error('验证失败');
|
||||
console.error('Captcha error:', error);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const reqData: ChatRequestData = {
|
||||
message: q,
|
||||
nonce: nonce || '',
|
||||
conversation_id: conversationId || '',
|
||||
app_type: 1,
|
||||
captcha_token: token,
|
||||
};
|
||||
|
||||
if (sseClientRef.current) {
|
||||
sseClientRef.current.subscribe(
|
||||
JSON.stringify(reqData),
|
||||
({ type, content, chunk_result }) => {
|
||||
if (type === 'conversation_id') {
|
||||
setConversationId(prev => prev + content);
|
||||
} else if (type === 'message_id') {
|
||||
messageIdRef.current += content;
|
||||
} else if (type === 'nonce') {
|
||||
setNonce(prev => prev + content);
|
||||
} else if (type === 'error') {
|
||||
setLoading(false);
|
||||
setIsChunkResult(false);
|
||||
setIsThinking(false);
|
||||
setThinking(4);
|
||||
setAnswer(prev => {
|
||||
if (content) {
|
||||
return prev + `\n\n回答出现错误:<error>${content}</error>`;
|
||||
}
|
||||
return prev + '\n\n回答出现错误,请重试';
|
||||
});
|
||||
if (content) message.error(content);
|
||||
} else if (type === 'done') {
|
||||
setAnswer(prevAnswer => {
|
||||
setThinkingContent(prevThinkingContent => {
|
||||
setChunkResult(prevChunkResult => {
|
||||
setConversation(prev => {
|
||||
const newConversation = [...prev];
|
||||
const lastConversation =
|
||||
newConversation[newConversation.length - 1];
|
||||
if (lastConversation) {
|
||||
lastConversation.a = prevAnswer;
|
||||
lastConversation.update_time = dayjs().format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
);
|
||||
lastConversation.message_id = messageIdRef.current;
|
||||
lastConversation.source = 'chat';
|
||||
lastConversation.chunk_result = prevChunkResult;
|
||||
lastConversation.thinking_content = prevThinkingContent;
|
||||
}
|
||||
return newConversation;
|
||||
});
|
||||
return prevChunkResult;
|
||||
});
|
||||
return prevThinkingContent;
|
||||
});
|
||||
return '';
|
||||
});
|
||||
|
||||
setFullAnswer('');
|
||||
setLoading(false);
|
||||
setIsChunkResult(false);
|
||||
setIsThinking(false);
|
||||
setThinking(4);
|
||||
} else if (type === 'data') {
|
||||
setIsChunkResult(false);
|
||||
setFullAnswer(prevFullAnswer => {
|
||||
const newFullAnswer = prevFullAnswer + content;
|
||||
const { thinkingContent, answerContent } =
|
||||
handleThinkingContent(newFullAnswer);
|
||||
|
||||
setThinkingContent(thinkingContent);
|
||||
setAnswer(answerContent);
|
||||
|
||||
if (newFullAnswer.includes('</think>')) {
|
||||
setIsThinking(false);
|
||||
setThinking(3);
|
||||
} else if (newFullAnswer.includes('<think>')) {
|
||||
setIsThinking(true);
|
||||
setThinking(2);
|
||||
} else {
|
||||
setThinking(3);
|
||||
}
|
||||
|
||||
return newFullAnswer;
|
||||
});
|
||||
} else if (type === 'chunk_result') {
|
||||
setChunkResult(prev => [...prev, chunk_result]);
|
||||
setIsChunkResult(true);
|
||||
setTimeout(scrollToBottom, 200);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
conversationId,
|
||||
nonce,
|
||||
messageIdRef,
|
||||
setConversationId,
|
||||
setNonce,
|
||||
setLoading,
|
||||
setThinking,
|
||||
setAnswer,
|
||||
setFullAnswer,
|
||||
setThinkingContent,
|
||||
setChunkResult,
|
||||
setConversation,
|
||||
setIsChunkResult,
|
||||
setIsThinking,
|
||||
scrollToBottom,
|
||||
],
|
||||
);
|
||||
|
||||
const handleSearchAbort = useCallback(() => {
|
||||
sseClientRef.current?.unsubscribe();
|
||||
setLoading(false);
|
||||
setThinking(4);
|
||||
}, [setLoading, setThinking]);
|
||||
|
||||
return {
|
||||
sseClientRef,
|
||||
initializeSSE,
|
||||
chatAnswer,
|
||||
handleSearchAbort,
|
||||
};
|
||||
};
|
||||
|
|
@ -247,7 +247,9 @@ const QaModal: React.FC<QaModalProps> = () => {
|
|||
<Box
|
||||
sx={{
|
||||
px: 3,
|
||||
pt: kbDetail?.settings?.disclaimer_settings?.content ? 2 : 0,
|
||||
pt: kbDetail?.settings?.web_app_custom_style?.show_brand_info
|
||||
? 2
|
||||
: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
|
@ -263,7 +265,10 @@ const QaModal: React.FC<QaModalProps> = () => {
|
|||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box>{kbDetail?.settings?.disclaimer_settings?.content}</Box>
|
||||
<Box>
|
||||
{kbDetail?.settings?.web_app_custom_style?.show_brand_info &&
|
||||
'本网站由 PandaWiki 提供技术支持'}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
Loading…
Reference in New Issue