Compare commits

..

1 Commits

Author SHA1 Message Date
xiaomakuaiz aa1ad4cb64
Merge 3f9124c649 into ca323de9b2 2025-11-12 11:53:21 +00:00
64 changed files with 1192 additions and 1201 deletions

View File

@ -1,6 +1,8 @@
package consts package consts
import ( import (
"math"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@ -12,12 +14,27 @@ type LicenseEdition int32
const ( const (
LicenseEditionFree LicenseEdition = 0 // 开源版 LicenseEditionFree LicenseEdition = 0 // 开源版
LicenseEditionProfession LicenseEdition = 1 // 专业 LicenseEditionContributor LicenseEdition = 1 // 联创
LicenseEditionEnterprise LicenseEdition = 2 // 企业版 LicenseEditionEnterprise LicenseEdition = 2 // 企业版
LicenseEditionBusiness LicenseEdition = 3 // 商业版
) )
func GetLicenseEdition(c echo.Context) LicenseEdition { func GetLicenseEdition(c echo.Context) LicenseEdition {
edition, _ := c.Get("edition").(LicenseEdition) edition, _ := c.Get("edition").(LicenseEdition)
return edition 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
}
}

View File

@ -4067,26 +4067,22 @@ const docTemplate = `{
"enum": [ "enum": [
0, 0,
1, 1,
2, 2
3
], ],
"x-enum-comments": { "x-enum-comments": {
"LicenseEditionBusiness": "商业版", "LicenseEditionContributor": "联创版",
"LicenseEditionEnterprise": "企业版", "LicenseEditionEnterprise": "企业版",
"LicenseEditionFree": "开源版", "LicenseEditionFree": "开源版"
"LicenseEditionProfession": "专业版"
}, },
"x-enum-descriptions": [ "x-enum-descriptions": [
"开源版", "开源版",
"专业版", "联创版",
"企业版", "企业版"
"商业版"
], ],
"x-enum-varnames": [ "x-enum-varnames": [
"LicenseEditionFree", "LicenseEditionFree",
"LicenseEditionProfession", "LicenseEditionContributor",
"LicenseEditionEnterprise", "LicenseEditionEnterprise"
"LicenseEditionBusiness"
] ]
}, },
"consts.ModelSettingMode": { "consts.ModelSettingMode": {

View File

@ -4060,26 +4060,22 @@
"enum": [ "enum": [
0, 0,
1, 1,
2, 2
3
], ],
"x-enum-comments": { "x-enum-comments": {
"LicenseEditionBusiness": "商业版", "LicenseEditionContributor": "联创版",
"LicenseEditionEnterprise": "企业版", "LicenseEditionEnterprise": "企业版",
"LicenseEditionFree": "开源版", "LicenseEditionFree": "开源版"
"LicenseEditionProfession": "专业版"
}, },
"x-enum-descriptions": [ "x-enum-descriptions": [
"开源版", "开源版",
"专业版", "联创版",
"企业版", "企业版"
"商业版"
], ],
"x-enum-varnames": [ "x-enum-varnames": [
"LicenseEditionFree", "LicenseEditionFree",
"LicenseEditionProfession", "LicenseEditionContributor",
"LicenseEditionEnterprise", "LicenseEditionEnterprise"
"LicenseEditionBusiness"
] ]
}, },
"consts.ModelSettingMode": { "consts.ModelSettingMode": {

View File

@ -117,24 +117,20 @@ definitions:
- 0 - 0
- 1 - 1
- 2 - 2
- 3
format: int32 format: int32
type: integer type: integer
x-enum-comments: x-enum-comments:
LicenseEditionBusiness: 商业 LicenseEditionContributor: 联创
LicenseEditionEnterprise: 企业版 LicenseEditionEnterprise: 企业版
LicenseEditionFree: 开源版 LicenseEditionFree: 开源版
LicenseEditionProfession: 专业版
x-enum-descriptions: x-enum-descriptions:
- 开源版 - 开源版
- 专业 - 联创
- 企业版 - 企业版
- 商业版
x-enum-varnames: x-enum-varnames:
- LicenseEditionFree - LicenseEditionFree
- LicenseEditionProfession - LicenseEditionContributor
- LicenseEditionEnterprise - LicenseEditionEnterprise
- LicenseEditionBusiness
consts.ModelSettingMode: consts.ModelSettingMode:
enum: enum:
- manual - manual

View File

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

View File

@ -6,6 +6,7 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/chaitin/panda-wiki/consts"
"github.com/chaitin/panda-wiki/domain" "github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/handler" "github.com/chaitin/panda-wiki/handler"
"github.com/chaitin/panda-wiki/log" "github.com/chaitin/panda-wiki/log"
@ -156,7 +157,7 @@ func (h *ShareCommentHandler) GetCommentList(c echo.Context) error {
} }
// 查询数据库获取所有评论-->0 所有, 12 为需要审核的评论 // 查询数据库获取所有评论-->0 所有, 12 为需要审核的评论
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID) commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID, consts.GetLicenseEdition(c))
if err != nil { if err != nil {
return h.NewResponseWithError(c, "failed to get comment list", err) return h.NewResponseWithError(c, "failed to get comment list", err)
} }

View File

@ -5,7 +5,6 @@ import (
v1 "github.com/chaitin/panda-wiki/api/kb/v1" v1 "github.com/chaitin/panda-wiki/api/kb/v1"
"github.com/chaitin/panda-wiki/consts" "github.com/chaitin/panda-wiki/consts"
"github.com/chaitin/panda-wiki/domain"
) )
// KBUserList // KBUserList
@ -56,8 +55,8 @@ func (h *KnowledgeBaseHandler) KBUserInvite(c echo.Context) error {
return h.NewResponseWithError(c, "validate request failed", err) return h.NewResponseWithError(c, "validate request failed", err)
} }
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl { if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil) return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
} }
err := h.usecase.KBUserInvite(c.Request().Context(), req) err := h.usecase.KBUserInvite(c.Request().Context(), req)
@ -88,8 +87,8 @@ func (h *KnowledgeBaseHandler) KBUserUpdate(c echo.Context) error {
return h.NewResponseWithError(c, "validate request failed", err) return h.NewResponseWithError(c, "validate request failed", err)
} }
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl { if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil) return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
} }
err := h.usecase.UpdateUserKB(c.Request().Context(), req) err := h.usecase.UpdateUserKB(c.Request().Context(), req)

View File

@ -91,7 +91,11 @@ func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c echo.Context) error {
return h.NewResponseWithError(c, "ports is required", nil) return h.NewResponseWithError(c, "ports is required", nil)
} }
req.MaxKB = domain.GetBaseEditionLimitation(c.Request().Context()).MaxKb req.MaxKB = 1
maxKB := c.Get("max_kb")
if maxKB != nil {
req.MaxKB = maxKB.(int)
}
did, err := h.usecase.CreateKnowledgeBase(c.Request().Context(), &req) did, err := h.usecase.CreateKnowledgeBase(c.Request().Context(), &req)
if err != nil { if err != nil {

View File

@ -46,7 +46,7 @@ func NewModelHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *
return handler return handler
} }
// GetModelList // get model list
// //
// @Summary get model list // @Summary get model list
// @Description get model list // @Description get model list
@ -66,7 +66,7 @@ func (h *ModelHandler) GetModelList(c echo.Context) error {
return h.NewResponseWithData(c, models) return h.NewResponseWithData(c, models)
} }
// CreateModel // create model
// //
// @Summary create model // @Summary create model
// @Description create model // @Description create model
@ -85,6 +85,9 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
return h.NewResponseWithError(c, "invalid request", err) 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() ctx := c.Request().Context()
param := domain.ModelParam{} param := domain.ModelParam{}
@ -109,7 +112,7 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
return h.NewResponseWithData(c, model) return h.NewResponseWithData(c, model)
} }
// UpdateModel // update model
// //
// @Description update model // @Description update model
// @Tags model // @Tags model
@ -127,6 +130,9 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
return h.NewResponseWithError(c, "invalid request", err) 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() ctx := c.Request().Context()
if err := h.usecase.Update(ctx, &req); err != nil { if err := h.usecase.Update(ctx, &req); err != nil {
return h.NewResponseWithError(c, "update model failed", err) return h.NewResponseWithError(c, "update model failed", err)
@ -134,7 +140,7 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
return h.NewResponseWithData(c, nil) return h.NewResponseWithData(c, nil)
} }
// CheckModel // check model
// //
// @Summary check model // @Summary check model
// @Description check model // @Description check model

View File

@ -81,13 +81,15 @@ func (h *NodeHandler) CreateNode(c echo.Context) error {
if err := c.Validate(req); err != nil { if err := c.Validate(req); err != nil {
return h.NewResponseWithError(c, "validate request body failed", err) return h.NewResponseWithError(c, "validate request body failed", err)
} }
req.MaxNode = 300
req.MaxNode = domain.GetBaseEditionLimitation(ctx).MaxNode if maxNode := c.Get("max_node"); maxNode != nil {
req.MaxNode = maxNode.(int)
}
id, err := h.usecase.Create(c.Request().Context(), req, authInfo.UserId) id, err := h.usecase.Create(c.Request().Context(), req, authInfo.UserId)
if err != nil { if err != nil {
if errors.Is(err, domain.ErrMaxNodeLimitReached) { if errors.Is(err, domain.ErrMaxNodeLimitReached) {
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到更高版本", nil) return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到联创版或企业版", nil)
} }
return h.NewResponseWithError(c, "create node failed", err) return h.NewResponseWithError(c, "create node failed", err)
} }

View File

@ -15,7 +15,7 @@ type AuthMiddleware interface {
Authorize(next echo.HandlerFunc) echo.HandlerFunc Authorize(next echo.HandlerFunc) echo.HandlerFunc
ValidateUserRole(role consts.UserRole) echo.MiddlewareFunc ValidateUserRole(role consts.UserRole) echo.MiddlewareFunc
ValidateKBUserPerm(role consts.UserKBPermission) 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) MustGetUserID(c echo.Context) (string, bool)
} }

View File

@ -6,7 +6,6 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
"slices"
"strings" "strings"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
@ -195,7 +194,7 @@ func (m *JWTMiddleware) ValidateKBUserPerm(perm consts.UserKBPermission) echo.Mi
} }
} }
func (m *JWTMiddleware) ValidateLicenseEdition(needEditions ...consts.LicenseEdition) echo.MiddlewareFunc { func (m *JWTMiddleware) ValidateLicenseEdition(needEdition consts.LicenseEdition) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -207,7 +206,7 @@ func (m *JWTMiddleware) ValidateLicenseEdition(needEditions ...consts.LicenseEdi
}) })
} }
if !slices.Contains(needEditions, edition) { if edition < needEdition {
return c.JSON(http.StatusForbidden, domain.PWResponse{ return c.JSON(http.StatusForbidden, domain.PWResponse{
Success: false, Success: false,
Message: "Unauthorized ValidateLicenseEdition", Message: "Unauthorized ValidateLicenseEdition",

@ -1 +1 @@
Subproject commit bcf7e0f0bedb18f43cf36463ddb45ace6c1dbab9 Subproject commit c4dc498df094cb617d31c95580db8239a445d652

View File

@ -300,8 +300,8 @@ func (r *AuthRepo) GetOrCreateAuth(ctx context.Context, auth *domain.Auth, sourc
return err return err
} }
if int(count) >= domain.GetBaseEditionLimitation(ctx).MaxSSOUser { 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, domain.GetBaseEditionLimitation(ctx).MaxSSOUser) return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, licenseEdition.GetMaxAuth(sourceType))
} }
auth.LastLoginTime = time.Now() auth.LastLoginTime = time.Now()

View File

@ -26,12 +26,12 @@ func (r *CommentRepository) CreateComment(ctx context.Context, comment *domain.C
return nil return nil
} }
func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string) ([]*domain.ShareCommentListItem, int64, error) { func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string, edition consts.LicenseEdition) ([]*domain.ShareCommentListItem, int64, error) {
// 按照时间排序来查询node_id的comments // 按照时间排序来查询node_id的comments
var comments []*domain.ShareCommentListItem var comments []*domain.ShareCommentListItem
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("node_id = ?", nodeID) query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("node_id = ?", nodeID)
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit { if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
query = query.Where("status = ?", domain.CommentStatusAccepted) //accepted query = query.Where("status = ?", domain.CommentStatusAccepted) //accepted
} }
@ -50,14 +50,14 @@ func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string) (
func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domain.CommentListReq, edition consts.LicenseEdition) ([]*domain.CommentListItem, int64, error) { func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domain.CommentListReq, edition consts.LicenseEdition) ([]*domain.CommentListItem, int64, error) {
comments := []*domain.CommentListItem{} comments := []*domain.CommentListItem{}
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("comments.kb_id = ?", req.KbID) query := r.db.Model(&domain.Comment{}).Where("comments.kb_id = ?", req.KbID)
var count int64 var count int64
if req.Status == nil { if req.Status == nil {
if err := query.Count(&count).Error; err != nil { if err := query.Count(&count).Error; err != nil {
return nil, 0, err return nil, 0, err
} }
} else { } else {
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit { if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
query = query.Where("comments.status = ?", *req.Status) query = query.Where("comments.status = ?", *req.Status)
} }
// 按照时间排序来查询kb_id的comments ->reject pending accepted // 按照时间排序来查询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 { func (r *CommentRepository) DeleteCommentList(ctx context.Context, commentID []string) error {
// 批量删除指定id的comment,获取删除的总的数量、 // 批量删除指定id的comment,获取删除的总的数量、
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("id IN (?)", commentID) query := r.db.Model(&domain.Comment{}).Where("id IN (?)", commentID)
if err := query.Delete(&domain.Comment{}).Error; err != nil { if err := query.Delete(&domain.Comment{}).Error; err != nil {
return err return err

View File

@ -344,7 +344,6 @@ func (r *KnowledgeBaseRepository) CreateKnowledgeBase(ctx context.Context, maxKB
Title: kb.Name, Title: kb.Name,
Desc: kb.Name, Desc: kb.Name,
Keyword: kb.Name, Keyword: kb.Name,
AutoSitemap: true,
Icon: domain.DefaultPandaWikiIconB64, Icon: domain.DefaultPandaWikiIconB64,
WelcomeStr: fmt.Sprintf("欢迎使用%s", kb.Name), WelcomeStr: fmt.Sprintf("欢迎使用%s", kb.Name),
Btns: []any{ Btns: []any{

View File

@ -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.kb_id = ?", kbID).
Where("kb_release_node_releases.release_id = ?", kbRelease.ID). Where("kb_release_node_releases.release_id = ?", kbRelease.ID).
Where("nodes.permissions->>'visible' != ?", consts.NodeAccessPermClosed). Where("nodes.permissions->>'visible' != ?", consts.NodeAccessPermClosed).
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"). 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").
Find(&nodes).Error; err != nil { Find(&nodes).Error; err != nil {
return nil, err return nil, err
} }

View File

@ -60,14 +60,18 @@ func (r *UserRepository) CreateUser(ctx context.Context, user *domain.User, edit
} }
user.Password = string(hashedPassword) user.Password = string(hashedPassword)
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionFree {
var count int64 var count int64
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil { if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
return err return err
} }
if count >= domain.GetBaseEditionLimitation(ctx).MaxAdmin { if edition == consts.LicenseEditionFree && count >= 1 {
return fmt.Errorf("exceed max admin limit, current count: %d, max limit: %d", count, domain.GetBaseEditionLimitation(ctx).MaxAdmin) return errors.New("free edition only allows 1 user")
}
if edition == consts.LicenseEditionContributor && count >= 5 {
return errors.New("contributor edition only allows 5 user")
}
} }
if err := tx.Create(user).Error; err != nil { if err := tx.Create(user).Error; err != nil {
return err return err
} }

View File

@ -88,38 +88,34 @@ func NewAppUsecase(
} }
func (u *AppUsecase) ValidateUpdateApp(ctx context.Context, id string, req *domain.UpdateAppReq, edition consts.LicenseEdition) error { 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) app, err := u.repo.GetAppDetail(ctx, id)
if err != nil { if err != nil {
return err return err
} }
if app.Settings.WatermarkContent != req.Settings.WatermarkContent ||
limitation := domain.GetBaseEditionLimitation(ctx) app.Settings.WatermarkSetting != req.Settings.WatermarkSetting ||
if !limitation.AllowCopyProtection && app.Settings.CopySetting != req.Settings.CopySetting { app.Settings.ContributeSettings != req.Settings.ContributeSettings ||
app.Settings.CopySetting != req.Settings.CopySetting {
return domain.ErrPermissionDenied return domain.ErrPermissionDenied
} }
case consts.LicenseEditionContributor:
if !limitation.AllowWatermark { app, err := u.repo.GetAppDetail(ctx, id)
if app.Settings.WatermarkSetting != req.Settings.WatermarkSetting || app.Settings.WatermarkContent != req.Settings.WatermarkContent { 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 return domain.ErrPermissionDenied
} }
case consts.LicenseEditionEnterprise:
return nil
default:
return fmt.Errorf("unsupported license type: %d", edition)
} }
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 return nil
} }
@ -622,8 +618,8 @@ func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId
} }
showBrand := true showBrand := true
defaultDisclaimer := "本回答由 PandaWiki 基于 AI 生成,仅供参考。" defaultDisclaimer := "本回答由 PandaWiki 基于 AI 生成,仅供参考。"
licenseEdition, _ := ctx.Value(consts.ContextKeyEdition).(consts.LicenseEdition)
if !domain.GetBaseEditionLimitation(ctx).AllowCustomCopyright { if licenseEdition < consts.LicenseEditionEnterprise {
appInfo.Settings.WebAppCustomSettings.ShowBrandInfo = &showBrand appInfo.Settings.WebAppCustomSettings.ShowBrandInfo = &showBrand
appInfo.Settings.DisclaimerSettings.Content = &defaultDisclaimer appInfo.Settings.DisclaimerSettings.Content = &defaultDisclaimer
} else { } else {

View File

@ -72,8 +72,8 @@ func (u *CommentUsecase) CreateComment(ctx context.Context, commentReq *domain.C
return CommentStr, nil return CommentStr, nil
} }
func (u *CommentUsecase) GetCommentListByNodeID(ctx context.Context, nodeID string) (*domain.PaginatedResult[[]*domain.ShareCommentListItem], error) { 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) comments, total, err := u.CommentRepo.GetCommentList(ctx, nodeID, edition)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -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 { func (u *NodeUsecase) ValidateNodePermissionsEdit(req v1.NodePermissionEditReq, edition consts.LicenseEdition) error {
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) { if edition != consts.LicenseEditionEnterprise {
if req.Permissions.Answerable == consts.NodeAccessPermPartial || req.Permissions.Visitable == consts.NodeAccessPermPartial || req.Permissions.Visible == consts.NodeAccessPermPartial { if req.Permissions.Answerable == consts.NodeAccessPermPartial || req.Permissions.Visitable == consts.NodeAccessPermPartial || req.Permissions.Visible == consts.NodeAccessPermPartial {
return domain.ErrPermissionDenied return domain.ErrPermissionDenied
} }

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"slices"
"sort" "sort"
"strconv" "strconv"
@ -68,12 +67,12 @@ func (u *StatUseCase) ValidateStatDay(statDay consts.StatDay, edition consts.Lic
case consts.StatDay1: case consts.StatDay1:
return nil return nil
case consts.StatDay7: case consts.StatDay7:
if edition == consts.LicenseEditionFree { if edition < consts.LicenseEditionContributor {
return domain.ErrPermissionDenied return domain.ErrPermissionDenied
} }
return nil return nil
case consts.StatDay30, consts.StatDay90: case consts.StatDay30, consts.StatDay90:
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) { if edition < consts.LicenseEditionEnterprise {
return domain.ErrPermissionDenied return domain.ErrPermissionDenied
} }
return nil return nil

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -144,7 +144,7 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
callback(); callback();
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [subscribe, appPreviewData, id]); }, [subscribe]);
return ( return (
<StyledCommonWrapper> <StyledCommonWrapper>

View File

@ -10,8 +10,6 @@ import { setAppPreviewData } from '@/store/slices/config';
import { DomainSocialMediaAccount } from '@/request/types'; import { DomainSocialMediaAccount } from '@/request/types';
import Switch from '../basicComponents/Switch'; import Switch from '../basicComponents/Switch';
import DragSocialInfo from '../basicComponents/DragSocialInfo'; import DragSocialInfo from '../basicComponents/DragSocialInfo';
import VersionMask from '@/components/VersionMask';
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
interface FooterConfigProps { interface FooterConfigProps {
data?: AppDetail | null; data?: AppDetail | null;
@ -77,6 +75,9 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
); );
const footer_show_intro = watch('footer_show_intro'); const footer_show_intro = watch('footer_show_intro');
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]);
useEffect(() => { useEffect(() => {
if (isEdit && appPreviewData) { if (isEdit && appPreviewData) {
setValue( setValue(
@ -505,7 +506,7 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
)} )}
/> />
</Stack> </Stack>
{isEnterprise && (
<Stack direction={'column'} gap={2}> <Stack direction={'column'} gap={2}>
<Box <Box
sx={{ sx={{
@ -528,10 +529,6 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
> >
PandaWiki PandaWiki
</Box> </Box>
<VersionMask
permission={PROFESSION_VERSION_PERMISSION}
sx={{ inset: '-8px 0' }}
>
<Controller <Controller
control={control} control={control}
name='show_brand_info' name='show_brand_info'
@ -551,6 +548,7 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
<Switch <Switch
sx={{ marginLeft: 'auto' }} sx={{ marginLeft: 'auto' }}
{...field} {...field}
disabled={!isEnterprise}
checked={field?.value === false ? false : true} checked={field?.value === false ? false : true}
onChange={e => { onChange={e => {
field.onChange(e.target.checked); field.onChange(e.target.checked);
@ -560,8 +558,8 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
</Stack> </Stack>
)} )}
/> />
</VersionMask>
</Stack> </Stack>
)}
</Stack> </Stack>
</> </>
); );

View File

@ -1,6 +1,5 @@
import { KnowledgeBaseListItem } from '@/api'; import { KnowledgeBaseListItem } from '@/api';
import { useURLSearchParams } from '@/hooks'; import { useURLSearchParams } from '@/hooks';
import { useFeatureValue } from '@/hooks';
import { ConstsUserRole } from '@/request/types'; import { ConstsUserRole } from '@/request/types';
import { useAppDispatch, useAppSelector } from '@/store'; import { useAppDispatch, useAppSelector } from '@/store';
import { setKbC, setKbId } from '@/store/slices/config'; import { setKbC, setKbId } from '@/store/slices/config';
@ -24,14 +23,14 @@ const KBSelect = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [_, setSearchParams] = useURLSearchParams(); const [_, setSearchParams] = useURLSearchParams();
const { kb_id, kbList, user } = useAppSelector(state => state.config); const { kb_id, kbList, license, user } = useAppSelector(
state => state.config,
);
const [modifyOpen, setModifyOpen] = useState(false); const [modifyOpen, setModifyOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [opraData, setOpraData] = useState<KnowledgeBaseListItem | null>(null); const [opraData, setOpraData] = useState<KnowledgeBaseListItem | null>(null);
const wikiCount = useFeatureValue('wikiCount');
return ( return (
<> <>
{(kbList || []).length > 0 && ( {(kbList || []).length > 0 && (
@ -122,7 +121,8 @@ const KBSelect = () => {
}} }}
fullWidth fullWidth
disabled={ disabled={
(kbList || []).length >= wikiCount || (license.edition === 0 && (kbList || []).length >= 1) ||
(license.edition === 1 && (kbList || []).length >= 3) ||
user.role === ConstsUserRole.UserRoleUser user.role === ConstsUserRole.UserRoleUser
} }
onClick={event => { onClick={event => {

View File

@ -3,20 +3,27 @@ import {
getApiV1License, getApiV1License,
deleteApiV1License, deleteApiV1License,
} from '@/request/pro/License'; } from '@/request/pro/License';
import { PostApiV1LicensePayload } from '@/request/pro/types';
import HelpCenter from '@/assets/json/help-center.json'; import HelpCenter from '@/assets/json/help-center.json';
import Takeoff from '@/assets/json/takeoff.json'; import Takeoff from '@/assets/json/takeoff.json';
import error from '@/assets/json/error.json'; import error from '@/assets/json/error.json';
import IconUpgrade from '@/assets/json/upgrade.json'; import IconUpgrade from '@/assets/json/upgrade.json';
import Upload from '@/components/UploadFile/Drag'; import Upload from '@/components/UploadFile/Drag';
import { useVersionInfo } from '@/hooks'; import { EditionType } from '@/constant/enums';
import { useAppDispatch, useAppSelector } from '@/store'; import { useAppDispatch, useAppSelector } from '@/store';
import { setLicense } from '@/store/slices/config'; import { setLicense } from '@/store/slices/config';
import { Box, Button, IconButton, Stack, TextField } from '@mui/material'; import {
Box,
Button,
IconButton,
MenuItem,
Stack,
TextField,
} from '@mui/material';
import { CusTabs, Icon, message, Modal } from '@ctzhian/ui'; import { CusTabs, Icon, message, Modal } from '@ctzhian/ui';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useState } from 'react'; import { useState } from 'react';
import LottieIcon from '../LottieIcon'; import LottieIcon from '../LottieIcon';
import { ConstsLicenseEdition } from '@/request/types';
interface AuthTypeModalProps { interface AuthTypeModalProps {
open: boolean; open: boolean;
@ -35,9 +42,10 @@ const AuthTypeModal = ({
const { license } = useAppSelector(state => state.config); const { license } = useAppSelector(state => state.config);
const [selected, setSelected] = useState<'file' | 'code'>( const [selected, setSelected] = useState<'file' | 'code'>(
license.edition === ConstsLicenseEdition.LicenseEditionEnterprise license.edition === 2 ? 'file' : 'code',
? 'file' );
: 'code', const [authVersion, setAuthVersion] = useState<'contributor' | 'enterprise'>(
license.edition === 2 ? 'enterprise' : 'contributor',
); );
const [updateOpen, setUpdateOpen] = useState(false); const [updateOpen, setUpdateOpen] = useState(false);
const [code, setCode] = useState(''); const [code, setCode] = useState('');
@ -45,15 +53,16 @@ const AuthTypeModal = ({
const [file, setFile] = useState<File | undefined>(undefined); const [file, setFile] = useState<File | undefined>(undefined);
const [unbindLoading, setUnbindLoading] = useState(false); const [unbindLoading, setUnbindLoading] = useState(false);
const versionInfo = useVersionInfo();
const handleSubmit = () => { const handleSubmit = () => {
setLoading(true); const params: PostApiV1LicensePayload = {
postApiV1License({ license_edition: authVersion,
license_type: selected, license_type: selected,
license_code: code, license_code: code,
license_file: file, license_file: file,
}) };
setLoading(true);
postApiV1License(params)
.then(() => { .then(() => {
message.success('激活成功'); message.success('激活成功');
setUpdateOpen(false); setUpdateOpen(false);
@ -139,8 +148,10 @@ const AuthTypeModal = ({
<Stack direction={'row'} alignItems={'center'}> <Stack direction={'row'} alignItems={'center'}>
<Box sx={{ width: 120, flexShrink: 0 }}></Box> <Box sx={{ width: 120, flexShrink: 0 }}></Box>
<Stack direction={'row'} alignItems={'center'} gap={2}> <Stack direction={'row'} alignItems={'center'} gap={2}>
<Box sx={{ minWidth: 50 }}>{versionInfo.label}</Box> <Box sx={{ minWidth: 50 }}>
{license.edition === ConstsLicenseEdition.LicenseEditionFree ? ( {EditionType[license.edition as keyof typeof EditionType].text}
</Box>
{license.edition === 0 ? (
<Stack direction={'row'} gap={2}> <Stack direction={'row'} gap={2}>
<Button <Button
size='small' size='small'
@ -229,7 +240,7 @@ const AuthTypeModal = ({
)} )}
</Stack> </Stack>
</Stack> </Stack>
{license.edition! !== ConstsLicenseEdition.LicenseEditionFree && ( {license.edition! > 0 && (
<Box> <Box>
<Stack direction={'row'} alignItems={'center'}> <Stack direction={'row'} alignItems={'center'}>
<Box sx={{ width: 120, flexShrink: 0 }}></Box> <Box sx={{ width: 120, flexShrink: 0 }}></Box>
@ -277,6 +288,18 @@ const AuthTypeModal = ({
value={selected} value={selected}
change={(v: string) => setSelected(v as 'file' | 'code')} 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' && ( {selected === 'code' && (
<TextField <TextField
sx={{ mt: 2 }} sx={{ mt: 2 }}

View File

@ -1,14 +1,24 @@
import HelpCenter from '@/assets/json/help-center.json'; import HelpCenter from '@/assets/json/help-center.json';
import IconUpgrade from '@/assets/json/upgrade.json'; import IconUpgrade from '@/assets/json/upgrade.json';
import LottieIcon from '@/components/LottieIcon'; import LottieIcon from '@/components/LottieIcon';
import { EditionType } from '@/constant/enums';
import { useAppSelector } from '@/store';
import { Box, Stack, Tooltip } from '@mui/material'; import { Box, Stack, Tooltip } from '@mui/material';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import packageJson from '../../../package.json'; import packageJson from '../../../package.json';
import AuthTypeModal from './AuthTypeModal'; import AuthTypeModal from './AuthTypeModal';
import { useVersionInfo } from '@/hooks'; 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,
};
const Version = () => { const Version = () => {
const versionInfo = useVersionInfo(); const { license } = useAppSelector(state => state.config);
const curVersion = import.meta.env.VITE_APP_VERSION || packageJson.version; const curVersion = import.meta.env.VITE_APP_VERSION || packageJson.version;
const [latestVersion, setLatestVersion] = useState<string | undefined>( const [latestVersion, setLatestVersion] = useState<string | undefined>(
undefined, undefined,
@ -47,8 +57,11 @@ const Version = () => {
> >
<Stack direction={'row'} alignItems='center' gap={0.5}> <Stack direction={'row'} alignItems='center' gap={0.5}>
<Box sx={{ width: 30, color: 'text.tertiary' }}></Box> <Box sx={{ width: 30, color: 'text.tertiary' }}></Box>
<img src={versionInfo.image} style={{ height: 13, marginTop: -1 }} /> <img
{versionInfo.label} src={versionMap[license.edition!]}
style={{ height: 13, marginTop: -1 }}
/>
{EditionType[license.edition as keyof typeof EditionType].text}
</Stack> </Stack>
<Stack direction={'row'} gap={0.5}> <Stack direction={'row'} gap={0.5}>
<Box sx={{ width: 30, color: 'text.tertiary' }}></Box> <Box sx={{ width: 30, color: 'text.tertiary' }}></Box>

View File

@ -9,8 +9,7 @@ import { Modal, message } from '@ctzhian/ui';
import { useState, useMemo, useEffect } from 'react'; import { useState, useMemo, useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useAppSelector } from '@/store'; import { useAppSelector } from '@/store';
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
import { VersionCanUse } from '@/components/VersionMask';
import { ConstsUserKBPermission, V1KBUserInviteReq } from '@/request/types'; import { ConstsUserKBPermission, V1KBUserInviteReq } from '@/request/types';
import { ConstsLicenseEdition } from '@/request/pro/types'; import { ConstsLicenseEdition } from '@/request/pro/types';
@ -27,13 +26,9 @@ const VERSION_MAP = {
message: '开源版只支持 1 个管理员', message: '开源版只支持 1 个管理员',
max: 1, max: 1,
}, },
[ConstsLicenseEdition.LicenseEditionProfession]: { [ConstsLicenseEdition.LicenseEditionContributor]: {
message: '专业版最多支持 20 个管理员', message: '联创版最多支持 3 个管理员',
max: 20, max: 3,
},
[ConstsLicenseEdition.LicenseEditionBusiness]: {
message: '商业版最多支持 50 个管理员',
max: 50,
}, },
}; };
@ -50,6 +45,9 @@ const MemberAdd = ({
const { kbList, license, refreshAdminRequest } = useAppSelector( const { kbList, license, refreshAdminRequest } = useAppSelector(
state => state.config, state => state.config,
); );
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]);
const { const {
control, control,
@ -120,10 +118,6 @@ const MemberAdd = ({
}); });
}); });
const isPro = useMemo(() => {
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
}, [license.edition]);
return ( return (
<> <>
<Button <Button
@ -259,14 +253,6 @@ const MemberAdd = ({
fullWidth fullWidth
displayEmpty displayEmpty
sx={{ height: 52 }} sx={{ height: 52 }}
MenuProps={{
sx: {
'.Mui-disabled': {
opacity: 1,
color: 'text.disabled',
},
},
}}
renderValue={(value: V1KBUserInviteReq['perm']) => { renderValue={(value: V1KBUserInviteReq['perm']) => {
return value ? ( return value ? (
PERM_MAP[value] PERM_MAP[value]
@ -280,25 +266,17 @@ const MemberAdd = ({
> >
</MenuItem> </MenuItem>
<MenuItem <MenuItem
disabled={!isEnterprise}
value={ConstsUserKBPermission.UserKBPermissionDocManage} value={ConstsUserKBPermission.UserKBPermissionDocManage}
disabled={!isPro}
> >
{' '} {isEnterprise ? '' : '(企业版可用)'}
<VersionCanUse
permission={PROFESSION_VERSION_PERMISSION}
/>
</MenuItem> </MenuItem>
<MenuItem <MenuItem
disabled={!isEnterprise}
value={ConstsUserKBPermission.UserKBPermissionDataOperate} value={ConstsUserKBPermission.UserKBPermissionDataOperate}
disabled={!isPro}
> >
{' '} {isEnterprise ? '' : '(企业版可用)'}
<VersionCanUse
permission={PROFESSION_VERSION_PERMISSION}
/>
</MenuItem> </MenuItem>
</Select> </Select>
)} )}

View File

@ -1,116 +0,0 @@
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;

View File

@ -797,6 +797,21 @@ export const FeedbackType = {
3: '其他', 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 = { export const DocWidth = {
full: { full: {
label: '全屏', label: '全屏',

View File

@ -1,293 +0,0 @@
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];
}

View File

@ -1,8 +1,3 @@
export { useBindCaptcha } from './useBindCaptcha'; export { useBindCaptcha } from './useBindCaptcha';
export { useCommitPendingInput } from './useCommitPendingInput'; export { useCommitPendingInput } from './useCommitPendingInput';
export { useURLSearchParams } from './useURLSearchParams'; export { useURLSearchParams } from './useURLSearchParams';
export {
useFeatureValue,
useFeatureValueSupported,
useVersionInfo,
} from './useVersionFeature';

View File

@ -1,34 +0,0 @@
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]
);
};

View File

@ -8,8 +8,6 @@ import { styled } from '@mui/material/styles';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import DocModal from './DocModal'; import DocModal from './DocModal';
import VersionMask from '@/components/VersionMask';
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
import { useURLSearchParams } from '@/hooks'; import { useURLSearchParams } from '@/hooks';
import { import {
@ -48,7 +46,7 @@ const statusColorMap = {
} as const; } as const;
export default function ContributionPage() { export default function ContributionPage() {
const { kb_id = '', license } = useAppSelector(state => state.config); const { kb_id = '', kbDetail } = useAppSelector(state => state.config);
const [searchParams, setSearchParams] = useURLSearchParams(); const [searchParams, setSearchParams] = useURLSearchParams();
const page = Number(searchParams.get('page') || '1'); const page = Number(searchParams.get('page') || '1');
const pageSize = Number(searchParams.get('page_size') || '20'); const pageSize = Number(searchParams.get('page_size') || '20');
@ -285,14 +283,12 @@ export default function ContributionPage() {
}; };
useEffect(() => { useEffect(() => {
if (kb_id && PROFESSION_VERSION_PERMISSION.includes(license.edition!)) if (kb_id) getData();
getData();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, pageSize, nodeNameParam, authNameParam, kb_id, license.edition]); }, [page, pageSize, nodeNameParam, authNameParam, kb_id]);
return ( return (
<Card> <Card>
<VersionMask permission={PROFESSION_VERSION_PERMISSION}>
<Stack <Stack
direction='row' direction='row'
alignItems={'center'} alignItems={'center'}
@ -392,7 +388,6 @@ export default function ContributionPage() {
onClose={() => setDocModalOpen(false)} onClose={() => setDocModalOpen(false)}
onOk={handleDocModalOk} onOk={handleDocModalOk}
/> />
</VersionMask>
</Card> </Card>
); );
} }

View File

@ -31,8 +31,6 @@ import {
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
import { VersionCanUse } from '@/components/VersionMask';
interface DocPropertiesModalProps { interface DocPropertiesModalProps {
open: boolean; open: boolean;
@ -42,6 +40,8 @@ interface DocPropertiesModalProps {
data: DomainNodeListItemResp[]; data: DomainNodeListItemResp[];
} }
const tips = '(企业版可用)';
const StyledText = styled('div')(({ theme }) => ({ const StyledText = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
fontSize: 16, fontSize: 16,
@ -53,12 +53,7 @@ const PER_OPTIONS = [
value: ConstsNodeAccessPerm.NodeAccessPermOpen, value: ConstsNodeAccessPerm.NodeAccessPermOpen,
}, },
{ {
label: ( label: '部分开放',
<Stack direction={'row'} alignItems={'center'}>
<span></span>
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
</Stack>
),
value: ConstsNodeAccessPerm.NodeAccessPermPartial, value: ConstsNodeAccessPerm.NodeAccessPermPartial,
}, },
{ {
@ -133,13 +128,13 @@ const DocPropertiesModal = ({
visitable: values.visitable as ConstsNodeAccessPerm, visitable: values.visitable as ConstsNodeAccessPerm,
visible: values.visible as ConstsNodeAccessPerm, visible: values.visible as ConstsNodeAccessPerm,
}, },
answerable_groups: isBusiness answerable_groups: isEnterprise
? values.answerable_groups.map(item => item.id!) ? values.answerable_groups.map(item => item.id!)
: undefined, : undefined,
visitable_groups: isBusiness visitable_groups: isEnterprise
? values.visitable_groups.map(item => item.id!) ? values.visitable_groups.map(item => item.id!)
: undefined, : undefined,
visible_groups: isBusiness visible_groups: isEnterprise
? values.visible_groups.map(item => item.id!) ? values.visible_groups.map(item => item.id!)
: undefined, : undefined,
}), }),
@ -158,15 +153,15 @@ const DocPropertiesModal = ({
}); });
}); });
const isBusiness = useMemo(() => { const isEnterprise = useMemo(() => {
return BUSINESS_VERSION_PERMISSION.includes(license.edition!); return license.edition === 2;
}, [license]); }, [license]);
const tree = filterEmptyFolders(convertToTree(data)); const tree = filterEmptyFolders(convertToTree(data));
useEffect(() => { useEffect(() => {
if (open && data) { if (open && data) {
if (isBusiness) { if (isEnterprise) {
getApiProV1AuthGroupList({ getApiProV1AuthGroupList({
kb_id: kb_id!, kb_id: kb_id!,
page: 1, page: 1,
@ -211,7 +206,7 @@ const DocPropertiesModal = ({
); );
}); });
} }
}, [open, data, isBusiness]); }, [open, data, isEnterprise]);
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
@ -307,15 +302,22 @@ const DocPropertiesModal = ({
name='answerable' name='answerable'
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<RadioGroup row {...field} sx={{ gap: 2 }}> <RadioGroup row {...field}>
{PER_OPTIONS.map(option => ( {PER_OPTIONS.map(option => (
<FormControlLabel <FormControlLabel
key={option.value} key={option.value}
value={option.value} value={option.value}
control={<Radio size='small' />} control={<Radio size='small' />}
label={option.label} label={
option.label +
(!isEnterprise &&
option.value ===
ConstsNodeAccessPerm.NodeAccessPermPartial
? tips
: '')
}
disabled={ disabled={
!isBusiness && !isEnterprise &&
option.value === option.value ===
ConstsNodeAccessPerm.NodeAccessPermPartial ConstsNodeAccessPerm.NodeAccessPermPartial
} }
@ -357,15 +359,22 @@ const DocPropertiesModal = ({
name='visitable' name='visitable'
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<RadioGroup row {...field} sx={{ gap: 2 }}> <RadioGroup row {...field}>
{PER_OPTIONS.map(option => ( {PER_OPTIONS.map(option => (
<FormControlLabel <FormControlLabel
key={option.value} key={option.value}
value={option.value} value={option.value}
control={<Radio size='small' />} control={<Radio size='small' />}
label={option.label} label={
option.label +
(!isEnterprise &&
option.value ===
ConstsNodeAccessPerm.NodeAccessPermPartial
? tips
: '')
}
disabled={ disabled={
!isBusiness && !isEnterprise &&
option.value === option.value ===
ConstsNodeAccessPerm.NodeAccessPermPartial ConstsNodeAccessPerm.NodeAccessPermPartial
} }
@ -407,15 +416,22 @@ const DocPropertiesModal = ({
name='visible' name='visible'
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<RadioGroup row {...field} sx={{ gap: 2 }}> <RadioGroup row {...field}>
{PER_OPTIONS.map(option => ( {PER_OPTIONS.map(option => (
<FormControlLabel <FormControlLabel
key={option.value} key={option.value}
value={option.value} value={option.value}
control={<Radio size='small' />} control={<Radio size='small' />}
label={option.label} label={
option.label +
(!isEnterprise &&
option.value ===
ConstsNodeAccessPerm.NodeAccessPermPartial
? tips
: '')
}
disabled={ disabled={
!isBusiness && !isEnterprise &&
option.value === option.value ===
ConstsNodeAccessPerm.NodeAccessPermPartial ConstsNodeAccessPerm.NodeAccessPermPartial
} }

View File

@ -22,8 +22,6 @@ import { useNavigate, useOutletContext } from 'react-router-dom';
import { WrapContext } from '..'; import { WrapContext } from '..';
import DocAddByCustomText from '../../component/DocAddByCustomText'; import DocAddByCustomText from '../../component/DocAddByCustomText';
import DocDelete from '../../component/DocDelete'; import DocDelete from '../../component/DocDelete';
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
import { VersionCanUse } from '@/components/VersionMask';
interface HeaderProps { interface HeaderProps {
edit: boolean; edit: boolean;
@ -54,8 +52,8 @@ const Header = ({
const [showSaveTip, setShowSaveTip] = useState(false); const [showSaveTip, setShowSaveTip] = useState(false);
const isBusiness = useMemo(() => { const isEnterprise = useMemo(() => {
return BUSINESS_VERSION_PERMISSION.includes(license.edition!); return license.edition === 2;
}, [license]); }, [license]);
const handlePublish = useCallback(() => { const handlePublish = useCallback(() => {
@ -311,7 +309,6 @@ const Header = ({
// }, // },
{ {
key: 'copy', key: 'copy',
textSx: { flex: 1 },
label: <StyledMenuSelect></StyledMenuSelect>, label: <StyledMenuSelect></StyledMenuSelect>,
onClick: () => { onClick: () => {
if (kb_id) { if (kb_id) {
@ -331,22 +328,26 @@ const Header = ({
}, },
{ {
key: 'version', key: 'version',
textSx: { flex: 1 },
label: ( label: (
<StyledMenuSelect disabled={!isBusiness}> <StyledMenuSelect disabled={!isEnterprise}>
{' '}
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} /> {!isEnterprise && (
<Tooltip title='企业版可用' placement='top' arrow>
<InfoIcon
sx={{ color: 'text.secondary', fontSize: 14 }}
/>
</Tooltip>
)}
</StyledMenuSelect> </StyledMenuSelect>
), ),
onClick: () => { onClick: () => {
if (isBusiness) { if (isEnterprise) {
navigate(`/doc/editor/history/${detail.id}`); navigate(`/doc/editor/history/${detail.id}`);
} }
}, },
}, },
{ {
key: 'rename', key: 'rename',
textSx: { flex: 1 },
label: <StyledMenuSelect></StyledMenuSelect>, label: <StyledMenuSelect></StyledMenuSelect>,
onClick: () => { onClick: () => {
setRenameOpen(true); setRenameOpen(true);
@ -354,7 +355,6 @@ const Header = ({
}, },
{ {
key: 'delete', key: 'delete',
textSx: { flex: 1 },
label: <StyledMenuSelect></StyledMenuSelect>, label: <StyledMenuSelect></StyledMenuSelect>,
onClick: () => { onClick: () => {
setDelOpen(true); setDelOpen(true);
@ -566,7 +566,7 @@ const StyledMenuSelect = styled('div')<{ disabled?: boolean }>(
padding: theme.spacing(0, 2), padding: theme.spacing(0, 2),
lineHeight: '40px', lineHeight: '40px',
height: 40, height: 40,
minWidth: 106, width: 106,
borderRadius: '5px', borderRadius: '5px',
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary, color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
cursor: disabled ? 'not-allowed' : 'pointer', cursor: disabled ? 'not-allowed' : 'pointer',

View File

@ -29,7 +29,6 @@ import Header from './Header';
import Summary from './Summary'; import Summary from './Summary';
import Toc from './Toc'; import Toc from './Toc';
import Toolbar from './Toolbar'; import Toolbar from './Toolbar';
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
interface WrapProps { interface WrapProps {
detail: V1NodeDetailResp; detail: V1NodeDetailResp;
@ -73,8 +72,8 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
emoji: defaultDetail.meta?.emoji || '', emoji: defaultDetail.meta?.emoji || '',
}); });
const isBusiness = useMemo(() => { const isEnterprise = useMemo(() => {
return BUSINESS_VERSION_PERMISSION.includes(license.edition!); return license.edition === 2;
}, [license]); }, [license]);
const debouncedUpdateSummary = useCallback( const debouncedUpdateSummary = useCallback(
@ -384,7 +383,7 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
</Stack> </Stack>
</Tooltip> </Tooltip>
)} )}
<Tooltip arrow title={isBusiness ? '查看历史版本' : ''}> <Tooltip arrow title={isEnterprise ? '查看历史版本' : ''}>
<Stack <Stack
direction={'row'} direction={'row'}
alignItems={'center'} alignItems={'center'}
@ -392,13 +391,13 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
sx={{ sx={{
fontSize: 12, fontSize: 12,
color: 'text.tertiary', color: 'text.tertiary',
cursor: isBusiness ? 'pointer' : 'text', cursor: isEnterprise ? 'pointer' : 'text',
':hover': { ':hover': {
color: isBusiness ? 'primary.main' : 'text.tertiary', color: isEnterprise ? 'primary.main' : 'text.tertiary',
}, },
}} }}
onClick={() => { onClick={() => {
if (isBusiness) { if (isEnterprise) {
navigate(`/doc/editor/history/${defaultDetail.id}`); navigate(`/doc/editor/history/${defaultDetail.id}`);
} }
}} }}

View File

@ -25,7 +25,6 @@ import {
IconButton, IconButton,
Stack, Stack,
useTheme, useTheme,
ButtonBase,
} from '@mui/material'; } from '@mui/material';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import VersionPublish from '../release/components/VersionPublish'; import VersionPublish from '../release/components/VersionPublish';
@ -419,19 +418,15 @@ const Content = () => {
> >
{publish.unpublished} / {publish.unpublished} /
</Box> </Box>
<ButtonBase <Button
disableRipple size='small'
sx={{ sx={{ minWidth: 0, p: 0, fontSize: 12 }}
fontSize: 12,
fontWeight: 400,
color: 'primary.main',
}}
onClick={() => { onClick={() => {
setPublishOpen(true); setPublishOpen(true);
}} }}
> >
</ButtonBase> </Button>
</> </>
)} )}
{ragReStartCount > 0 && ( {ragReStartCount > 0 && (

View File

@ -28,7 +28,6 @@ import {
ButtonBase, ButtonBase,
} from '@mui/material'; } from '@mui/material';
import { Ellipsis, Table, Modal, Icon, message } from '@ctzhian/ui'; import { Ellipsis, Table, Modal, Icon, message } from '@ctzhian/ui';
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useEffect, useState, useMemo } from 'react'; import { useEffect, useState, useMemo } from 'react';
@ -163,8 +162,8 @@ const Comments = ({
useState<DomainWebAppCommentSettings | null>(null); useState<DomainWebAppCommentSettings | null>(null);
const isEnableReview = useMemo(() => { const isEnableReview = useMemo(() => {
return PROFESSION_VERSION_PERMISSION.includes(license.edition!); return !!(license.edition === 1 || license.edition === 2);
}, [license.edition]); }, [license]);
useEffect(() => { useEffect(() => {
setShowCommentsFilter(isEnableReview); setShowCommentsFilter(isEnableReview);

View File

@ -14,8 +14,6 @@ import dayjs from 'dayjs';
import { ColumnType } from '@ctzhian/ui/dist/Table'; import { ColumnType } from '@ctzhian/ui/dist/Table';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useAppSelector } from '@/store'; import { useAppSelector } from '@/store';
import { VersionCanUse } from '@/components/VersionMask';
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
interface AddRoleProps { interface AddRoleProps {
open: boolean; open: boolean;
@ -25,8 +23,7 @@ interface AddRoleProps {
} }
const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => { const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
const { kb_id } = useAppSelector(state => state.config); const { kb_id, license } = useAppSelector(state => state.config);
const { license } = useAppSelector(state => state.config);
const [list, setList] = useState<V1UserListItemResp[]>([]); const [list, setList] = useState<V1UserListItemResp[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<string>(''); const [selectedRowKeys, setSelectedRowKeys] = useState<string>('');
@ -34,6 +31,10 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
ConstsUserKBPermission.UserKBPermissionFullControl, ConstsUserKBPermission.UserKBPermissionFullControl,
); );
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]);
const columns: ColumnType<V1UserListItemResp>[] = [ const columns: ColumnType<V1UserListItemResp>[] = [
{ {
title: '', title: '',
@ -118,10 +119,6 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
} }
}, [open]); }, [open]);
const isPro = useMemo(() => {
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
}, [license.edition]);
return ( return (
<Modal <Modal
title='添加 Wiki 站管理员' title='添加 Wiki 站管理员'
@ -212,33 +209,22 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
fullWidth fullWidth
sx={{ height: 52 }} sx={{ height: 52 }}
value={perm} value={perm}
MenuProps={{
sx: {
'.Mui-disabled': {
opacity: 1,
color: 'text.disabled',
},
},
}}
onChange={e => setPerm(e.target.value as V1KBUserInviteReq['perm'])} onChange={e => setPerm(e.target.value as V1KBUserInviteReq['perm'])}
> >
<MenuItem value={ConstsUserKBPermission.UserKBPermissionFullControl}> <MenuItem value={ConstsUserKBPermission.UserKBPermissionFullControl}>
</MenuItem> </MenuItem>
<MenuItem <MenuItem
disabled={!isEnterprise}
value={ConstsUserKBPermission.UserKBPermissionDocManage} value={ConstsUserKBPermission.UserKBPermissionDocManage}
disabled={!isPro}
> >
{' '} {isEnterprise ? '' : '(企业版可用)'}
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
</MenuItem> </MenuItem>
<MenuItem <MenuItem
disabled={!isEnterprise}
value={ConstsUserKBPermission.UserKBPermissionDataOperate} value={ConstsUserKBPermission.UserKBPermissionDataOperate}
disabled={!isPro}
> >
{' '} {isEnterprise ? '' : '(企业版可用)'}
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
</MenuItem> </MenuItem>
</Select> </Select>
</FormItem> </FormItem>

View File

@ -1,6 +1,5 @@
import { getApiProV1Prompt, postApiProV1Prompt } from '@/request/pro/Prompt'; import { getApiProV1Prompt, postApiProV1Prompt } from '@/request/pro/Prompt';
import { DomainKnowledgeBaseDetail } from '@/request/types'; import { DomainKnowledgeBaseDetail } from '@/request/types';
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
import { useAppSelector } from '@/store'; import { useAppSelector } from '@/store';
import { message } from '@ctzhian/ui'; import { message } from '@ctzhian/ui';
import { Box, Slider, TextField } from '@mui/material'; import { Box, Slider, TextField } from '@mui/material';
@ -34,12 +33,11 @@ const CardAI = ({ kb }: CardAIProps) => {
}); });
const isPro = useMemo(() => { const isPro = useMemo(() => {
return PROFESSION_VERSION_PERMISSION.includes(license.edition!); return license.edition === 1 || license.edition === 2;
}, [license]); }, [license]);
useEffect(() => { useEffect(() => {
if (!kb.id || !PROFESSION_VERSION_PERMISSION.includes(license.edition!)) if (!kb.id || !isPro) return;
return;
getApiProV1Prompt({ kb_id: kb.id! }).then(res => { getApiProV1Prompt({ kb_id: kb.id! }).then(res => {
setValue('content', res.content || ''); setValue('content', res.content || '');
}); });
@ -56,7 +54,7 @@ const CardAI = ({ kb }: CardAIProps) => {
<SettingCardItem title='智能问答' isEdit={isEdit} onSubmit={onSubmit}> <SettingCardItem title='智能问答' isEdit={isEdit} onSubmit={onSubmit}>
<FormItem <FormItem
vertical vertical
permission={PROFESSION_VERSION_PERMISSION} tooltip={!isPro && '联创版和企业版可用'}
extra={ extra={
<Box <Box
sx={{ sx={{

View File

@ -29,8 +29,6 @@ import { ColumnType } from '@ctzhian/ui/dist/Table';
import { useEffect, useMemo, useState, useRef } from 'react'; import { useEffect, useMemo, useState, useRef } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useAppSelector } from '@/store'; import { useAppSelector } from '@/store';
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
import { VersionCanUse } from '@/components/VersionMask';
import { SettingCardItem, FormItem } from './Common'; import { SettingCardItem, FormItem } from './Common';
interface CardAuthProps { interface CardAuthProps {
@ -116,7 +114,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
}), }),
value.enabled === '2' && value.enabled === '2' &&
source_type !== EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword source_type !== EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword
? isBusiness ? isPro
? postApiProV1AuthSet({ ? postApiProV1AuthSet({
kb_id, kb_id,
source_type: value.source_type as ConstsSourceType, source_type: value.source_type as ConstsSourceType,
@ -159,18 +157,25 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
}); });
}); });
const isBusiness = useMemo(() => { const isPro = useMemo(() => {
return BUSINESS_VERSION_PERMISSION.includes(license.edition!); return license.edition === 1 || license.edition === 2;
}, [license]);
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]); }, [license]);
useEffect(() => { useEffect(() => {
const source_type = isBusiness const source_type = isPro
? kb.access_settings?.source_type || ? kb.access_settings?.source_type ||
EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword 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); setValue('source_type', source_type);
sourceTypeRef.current = source_type; sourceTypeRef.current = source_type;
}, [kb, isBusiness]); }, [kb, isPro]);
useEffect(() => { useEffect(() => {
if (kb.access_settings?.simple_auth) { if (kb.access_settings?.simple_auth) {
@ -186,7 +191,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
}, [kb]); }, [kb]);
const getAuth = () => { const getAuth = () => {
if (isBusiness) { if (isPro) {
getApiProV1AuthGet({ getApiProV1AuthGet({
kb_id, kb_id,
source_type: source_type as ConstsSourceType, source_type: source_type as ConstsSourceType,
@ -231,7 +236,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
useEffect(() => { useEffect(() => {
if (!kb_id || enabled !== '2') return; if (!kb_id || enabled !== '2') return;
getAuth(); getAuth();
}, [kb_id, isBusiness, source_type, enabled]); }, [kb_id, isPro, source_type, enabled]);
const columns: ColumnType<GithubComChaitinPandaWikiProApiAuthV1AuthItem>[] = [ const columns: ColumnType<GithubComChaitinPandaWikiProApiAuthV1AuthItem>[] = [
{ {
@ -870,18 +875,8 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
field.onChange(e.target.value); field.onChange(e.target.value);
setIsEdit(true); setIsEdit(true);
}} }}
MenuProps={{
sx: {
'.Mui-disabled': {
opacity: 1,
color: 'text.disabled',
},
},
}}
fullWidth fullWidth
sx={{ sx={{ height: 52 }}
height: 52,
}}
> >
<MenuItem <MenuItem
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword} value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword}
@ -890,52 +885,44 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
</MenuItem> </MenuItem>
<MenuItem <MenuItem
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeDingTalk} value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeDingTalk}
disabled={!isBusiness} disabled={!isPro}
> >
{' '} {isPro ? '' : tips}
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
</MenuItem> </MenuItem>
<MenuItem <MenuItem
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeFeishu} value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeFeishu}
disabled={!isBusiness} disabled={!isPro}
> >
{' '} {isPro ? '' : tips}
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
</MenuItem> </MenuItem>
<MenuItem <MenuItem
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeWeCom} value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeWeCom}
disabled={!isBusiness} disabled={!isPro}
> >
{' '} {isPro ? '' : tips}
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
</MenuItem> </MenuItem>
<MenuItem <MenuItem
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeOAuth} value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeOAuth}
disabled={!isBusiness} disabled={!isPro}
> >
OAuth {' '} OAuth {isPro ? '' : tips}
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
</MenuItem> </MenuItem>
<MenuItem <MenuItem
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeCAS} value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeCAS}
disabled={!isBusiness} disabled={!isPro}
> >
CAS {' '} CAS {isPro ? '' : tips}
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
</MenuItem> </MenuItem>
<MenuItem <MenuItem
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeLDAP} value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeLDAP}
disabled={!isBusiness} disabled={!isPro}
> >
LDAP {' '} LDAP {isPro ? '' : tips}
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
</MenuItem> </MenuItem>
<MenuItem <MenuItem
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub} value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub}
disabled={!isBusiness}
> >
GitHub {' '} GitHub
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
</MenuItem> </MenuItem>
</Select> </Select>
)} )}

View File

@ -3,7 +3,7 @@ import {
DomainKnowledgeBaseDetail, DomainKnowledgeBaseDetail,
} from '@/request/types'; } from '@/request/types';
import { useAppSelector } from '@/store'; import { useAppSelector } from '@/store';
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version'; import InfoIcon from '@mui/icons-material/Info';
import { import {
Box, Box,
Chip, Chip,
@ -12,6 +12,7 @@ import {
RadioGroup, RadioGroup,
styled, styled,
TextField, TextField,
Tooltip,
} from '@mui/material'; } from '@mui/material';
import { getApiV1AppDetail, putApiV1App } from '@/request/App'; import { getApiV1AppDetail, putApiV1App } from '@/request/App';
@ -36,7 +37,7 @@ const DocumentComments = ({
data: DomainAppDetailResp; data: DomainAppDetailResp;
refresh: () => void; refresh: () => void;
}) => { }) => {
const { kb_id } = useAppSelector(state => state.config); const { license, kb_id } = useAppSelector(state => state.config);
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const { control, handleSubmit, setValue } = useForm({ const { control, handleSubmit, setValue } = useForm({
defaultValues: { defaultValues: {
@ -56,6 +57,8 @@ const DocumentComments = ({
); );
}, [data]); }, [data]);
const isPro = license.edition === 1 || license.edition === 2;
const onSubmit = handleSubmit(formData => { const onSubmit = handleSubmit(formData => {
putApiV1App( putApiV1App(
{ id: data.id! }, { id: data.id! },
@ -105,7 +108,7 @@ const DocumentComments = ({
)} )}
/> />
</FormItem> </FormItem>
<FormItem label='评论审核' permission={PROFESSION_VERSION_PERMISSION}> <FormItem label='评论审核' tooltip={!isPro && '联创版和企业版可用'}>
<Controller <Controller
control={control} control={control}
name='moderation_enable' name='moderation_enable'
@ -113,6 +116,7 @@ const DocumentComments = ({
<RadioGroup <RadioGroup
row row
{...field} {...field}
value={isPro ? field.value : undefined}
onChange={e => { onChange={e => {
setIsEdit(true); setIsEdit(true);
field.onChange(+e.target.value as 1 | 0); field.onChange(+e.target.value as 1 | 0);
@ -120,12 +124,12 @@ const DocumentComments = ({
> >
<FormControlLabel <FormControlLabel
value={1} value={1}
control={<Radio size='small' />} control={<Radio size='small' disabled={!isPro} />}
label={<StyledRadioLabel></StyledRadioLabel>} label={<StyledRadioLabel></StyledRadioLabel>}
/> />
<FormControlLabel <FormControlLabel
value={0} value={0}
control={<Radio size='small' />} control={<Radio size='small' disabled={!isPro} />}
label={<StyledRadioLabel></StyledRadioLabel>} label={<StyledRadioLabel></StyledRadioLabel>}
/> />
</RadioGroup> </RadioGroup>
@ -146,7 +150,7 @@ const AIQuestion = ({
refresh: () => void; refresh: () => void;
}) => { }) => {
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const { kb_id } = useAppSelector(state => state.config); const { kb_id, license } = useAppSelector(state => state.config);
const { control, handleSubmit, setValue } = useForm({ const { control, handleSubmit, setValue } = useForm({
defaultValues: { defaultValues: {
is_enabled: true, is_enabled: true,
@ -155,6 +159,7 @@ const AIQuestion = ({
}, },
}); });
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const isEnterprise = license.edition === 2;
const onSubmit = handleSubmit(formData => { const onSubmit = handleSubmit(formData => {
putApiV1App( putApiV1App(
@ -268,7 +273,7 @@ const AIQuestion = ({
)} )}
/>{' '} />{' '}
</FormItem> </FormItem>
<FormItem label='免责声明' permission={PROFESSION_VERSION_PERMISSION}> <FormItem label='免责声明' tooltip={!isEnterprise && '企业版可用'}>
<Controller <Controller
control={control} control={control}
name='disclaimer' name='disclaimer'
@ -277,6 +282,7 @@ const AIQuestion = ({
{...field} {...field}
fullWidth fullWidth
value={field.value || ''} value={field.value || ''}
disabled={!isEnterprise}
placeholder='请输入免责声明' placeholder='请输入免责声明'
onChange={e => { onChange={e => {
setIsEdit(true); setIsEdit(true);
@ -298,7 +304,7 @@ const DocumentContribution = ({
refresh: () => void; refresh: () => void;
}) => { }) => {
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const { kb_id } = useAppSelector(state => state.config); const { license, kb_id } = useAppSelector(state => state.config);
const { control, handleSubmit, setValue } = useForm({ const { control, handleSubmit, setValue } = useForm({
defaultValues: { defaultValues: {
is_enable: false, is_enable: false,
@ -324,6 +330,7 @@ const DocumentContribution = ({
}); });
}); });
const isPro = license.edition === 1 || license.edition === 2;
useEffect(() => { useEffect(() => {
setValue( setValue(
'is_enable', 'is_enable',
@ -333,8 +340,21 @@ const DocumentContribution = ({
}, [data]); }, [data]);
return ( return (
<SettingCardItem title='文档贡献' isEdit={isEdit} onSubmit={onSubmit}> <SettingCardItem
<FormItem label='文档贡献' permission={PROFESSION_VERSION_PERMISSION}> title={
<>
{!isPro && (
<Tooltip title='联创版和企业版可用' placement='top' arrow>
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14 }} />
</Tooltip>
)}
</>
}
isEdit={isEdit}
onSubmit={onSubmit}
>
<FormItem label='文档贡献'>
<Controller <Controller
control={control} control={control}
name='is_enable' name='is_enable'
@ -342,7 +362,7 @@ const DocumentContribution = ({
<RadioGroup <RadioGroup
row row
{...field} {...field}
value={field.value} value={isPro ? field.value : undefined}
onChange={e => { onChange={e => {
setIsEdit(true); setIsEdit(true);
field.onChange(e.target.value === 'true'); field.onChange(e.target.value === 'true');
@ -350,12 +370,12 @@ const DocumentContribution = ({
> >
<FormControlLabel <FormControlLabel
value={true} value={true}
control={<Radio size='small' />} control={<Radio size='small' disabled={!isPro} />}
label={<StyledRadioLabel></StyledRadioLabel>} label={<StyledRadioLabel></StyledRadioLabel>}
/> />
<FormControlLabel <FormControlLabel
value={false} value={false}
control={<Radio size='small' />} control={<Radio size='small' disabled={!isPro} />}
label={<StyledRadioLabel></StyledRadioLabel>} label={<StyledRadioLabel></StyledRadioLabel>}
/> />
</RadioGroup> </RadioGroup>

View File

@ -38,10 +38,6 @@ import { Controller, useForm } from 'react-hook-form';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import AddRole from './AddRole'; import AddRole from './AddRole';
import { Form, FormItem, SettingCardItem } from './Common'; import { Form, FormItem, SettingCardItem } from './Common';
import {
PROFESSION_VERSION_PERMISSION,
BUSINESS_VERSION_PERMISSION,
} from '@/constant/version';
type ApiTokenPermission = type ApiTokenPermission =
GithubComChaitinPandaWikiProApiTokenV1CreateAPITokenReq['permission']; GithubComChaitinPandaWikiProApiTokenV1CreateAPITokenReq['permission'];
@ -73,8 +69,8 @@ const ApiToken = () => {
perm: ConstsUserKBPermission.UserKBPermissionFullControl, perm: ConstsUserKBPermission.UserKBPermissionFullControl,
}, },
}); });
const isBusiness = useMemo(() => { const isEnterprise = useMemo(() => {
return BUSINESS_VERSION_PERMISSION.includes(license.edition!); return license.edition === 2;
}, [license]); }, [license]);
const onDeleteApiToken = (id: string, name: string) => { const onDeleteApiToken = (id: string, name: string) => {
@ -135,9 +131,9 @@ const ApiToken = () => {
}; };
useEffect(() => { useEffect(() => {
if (!kb_id || !isBusiness) return; if (!kb_id) return;
getApiTokenList(); getApiTokenList();
}, [kb_id, isBusiness]); }, [kb_id]);
useEffect(() => { useEffect(() => {
if (!addOpen) reset(); if (!addOpen) reset();
@ -146,17 +142,27 @@ const ApiToken = () => {
return ( return (
<SettingCardItem <SettingCardItem
title='API Token' title='API Token'
permission={BUSINESS_VERSION_PERMISSION}
extra={ extra={
<Stack direction={'row'} alignItems={'center'}> <Stack direction={'row'} alignItems={'center'}>
<Button <Button
color='primary' color='primary'
size='small' size='small'
disabled={!isEnterprise}
onClick={() => setAddOpen(true)} onClick={() => setAddOpen(true)}
sx={{ textTransform: 'none' }} sx={{ textTransform: 'none' }}
> >
API Token API Token
</Button> </Button>
<Tooltip title={'企业版可用'} placement='top' arrow>
<InfoIcon
sx={{
color: 'text.secondary',
fontSize: 14,
display: !isEnterprise ? 'block' : 'none',
}}
/>
</Tooltip>
</Stack> </Stack>
} }
> >
@ -226,7 +232,7 @@ const ApiToken = () => {
size='small' size='small'
sx={{ width: 120 }} sx={{ width: 120 }}
value={it.permission} value={it.permission}
disabled={!isBusiness || user.role !== 'admin'} disabled={!isEnterprise || user.role !== 'admin'}
onChange={e => onChange={e =>
onUpdateApiToken(it.id!, e.target.value as ApiTokenPermission) onUpdateApiToken(it.id!, e.target.value as ApiTokenPermission)
} }
@ -253,7 +259,7 @@ const ApiToken = () => {
kbDetail?.perm !== kbDetail?.perm !==
ConstsUserKBPermission.UserKBPermissionFullControl ConstsUserKBPermission.UserKBPermissionFullControl
? '权限不足' ? '权限不足'
: '业版可用' : '业版可用'
} }
placement='top' placement='top'
arrow arrow
@ -264,7 +270,7 @@ const ApiToken = () => {
fontSize: 14, fontSize: 14,
ml: 1, ml: 1,
visibility: visibility:
!isBusiness || !isEnterprise ||
kbDetail?.perm !== kbDetail?.perm !==
ConstsUserKBPermission.UserKBPermissionFullControl ConstsUserKBPermission.UserKBPermissionFullControl
? 'visible' ? 'visible'
@ -279,13 +285,13 @@ const ApiToken = () => {
type='icon-icon_tool_close' type='icon-icon_tool_close'
sx={{ sx={{
cursor: cursor:
!isBusiness || !isEnterprise ||
kbDetail?.perm !== kbDetail?.perm !==
ConstsUserKBPermission.UserKBPermissionFullControl ConstsUserKBPermission.UserKBPermissionFullControl
? 'not-allowed' ? 'not-allowed'
: 'pointer', : 'pointer',
color: color:
!isBusiness || !isEnterprise ||
kbDetail?.perm !== kbDetail?.perm !==
ConstsUserKBPermission.UserKBPermissionFullControl ConstsUserKBPermission.UserKBPermissionFullControl
? 'text.disabled' ? 'text.disabled'
@ -293,7 +299,7 @@ const ApiToken = () => {
}} }}
onClick={() => { onClick={() => {
if ( if (
!isBusiness || !isEnterprise ||
kbDetail?.perm !== kbDetail?.perm !==
ConstsUserKBPermission.UserKBPermissionFullControl ConstsUserKBPermission.UserKBPermissionFullControl
) )
@ -361,16 +367,17 @@ const ApiToken = () => {
> >
</MenuItem> </MenuItem>
<MenuItem <MenuItem
disabled={!isEnterprise}
value={ConstsUserKBPermission.UserKBPermissionDocManage} value={ConstsUserKBPermission.UserKBPermissionDocManage}
> >
{isEnterprise ? '' : '(企业版可用)'}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
disabled={!isEnterprise}
value={ConstsUserKBPermission.UserKBPermissionDataOperate} value={ConstsUserKBPermission.UserKBPermissionDataOperate}
> >
{isEnterprise ? '' : '(企业版可用)'}
</MenuItem> </MenuItem>
</Select> </Select>
); );
@ -398,9 +405,9 @@ const CardKB = () => {
}); });
}; };
const isPro = useMemo(() => { const isEnterprise = useMemo(() => {
return PROFESSION_VERSION_PERMISSION.includes(license.edition!); return license.edition === 2;
}, [license.edition]); }, [license]);
useEffect(() => { useEffect(() => {
if (!kb_id) return; if (!kb_id) return;
@ -506,7 +513,7 @@ const CardKB = () => {
size='small' size='small'
sx={{ width: 180 }} sx={{ width: 180 }}
value={it.perms} value={it.perms}
disabled={!isPro || it.role === 'admin'} disabled={!isEnterprise || it.role === 'admin'}
onChange={e => onChange={e =>
onUpdateUserPermission( onUpdateUserPermission(
it.id!, it.id!,
@ -535,7 +542,7 @@ const CardKB = () => {
title={ title={
it.role === 'admin' it.role === 'admin'
? '超级管理员不可被修改权限' ? '超级管理员不可被修改权限'
: '业版可用' : '业版可用'
} }
placement='top' placement='top'
arrow arrow
@ -546,7 +553,9 @@ const CardKB = () => {
fontSize: 14, fontSize: 14,
ml: 1, ml: 1,
visibility: visibility:
!isPro || it.role === 'admin' ? 'visible' : 'hidden', !isEnterprise || it.role === 'admin'
? 'visible'
: 'hidden',
}} }}
/> />
</Tooltip> </Tooltip>

View File

@ -1,6 +1,7 @@
import { DomainKnowledgeBaseDetail } from '@/request/types'; import { DomainKnowledgeBaseDetail } from '@/request/types';
import { import {
Box, Box,
Button,
FormControl, FormControl,
FormControlLabel, FormControlLabel,
Link, Link,
@ -12,11 +13,10 @@ import {
import ShowText from '@/components/ShowText'; import ShowText from '@/components/ShowText';
import { getApiV1AppDetail, putApiV1App } from '@/request/App'; import { getApiV1AppDetail, putApiV1App } from '@/request/App';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { FormItem, SettingCardItem } from './Common'; import { FormItem, SettingCardItem } from './Common';
import { DomainAppDetailResp } from '@/request/types'; import { DomainAppDetailResp } from '@/request/types';
import { message } from '@ctzhian/ui'; import { message } from '@ctzhian/ui';
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
import { useAppSelector } from '@/store'; import { useAppSelector } from '@/store';
const CardRobotApi = ({ const CardRobotApi = ({
@ -29,6 +29,11 @@ const CardRobotApi = ({
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null); const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
const { license } = useAppSelector(state => state.config); const { license } = useAppSelector(state => state.config);
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]);
const { const {
control, control,
handleSubmit, handleSubmit,
@ -109,7 +114,10 @@ const CardRobotApi = ({
} }
onSubmit={onSubmit} onSubmit={onSubmit}
> >
<FormItem label='问答机器人 API' permission={BUSINESS_VERSION_PERMISSION}> <FormItem
label='问答机器人 API'
tooltip={!isEnterprise ? '企业版可用' : undefined}
>
<FormControl> <FormControl>
<Controller <Controller
control={control} control={control}
@ -125,11 +133,13 @@ const CardRobotApi = ({
<Stack direction={'row'}> <Stack direction={'row'}>
<FormControlLabel <FormControlLabel
value={true} value={true}
disabled={!isEnterprise}
control={<Radio size='small' />} control={<Radio size='small' />}
label={<Box sx={{ width: 100 }}></Box>} label={<Box sx={{ width: 100 }}></Box>}
/> />
<FormControlLabel <FormControlLabel
value={false} value={false}
disabled={!isEnterprise}
control={<Radio size='small' />} control={<Radio size='small' />}
label={<Box sx={{ width: 100 }}></Box>} label={<Box sx={{ width: 100 }}></Box>}
/> />
@ -140,7 +150,7 @@ const CardRobotApi = ({
</FormControl> </FormControl>
</FormItem> </FormItem>
{isEnabled && BUSINESS_VERSION_PERMISSION.includes(license.edition!) && ( {isEnabled && (
<> <>
<FormItem label='API Token' required> <FormItem label='API Token' required>
<Controller <Controller

View File

@ -19,8 +19,6 @@ import {
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { FormItem, SettingCardItem } from './Common'; import { FormItem, SettingCardItem } from './Common';
import VersionMask from '@/components/VersionMask';
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
const CardRobotWecomService = ({ const CardRobotWecomService = ({
kb, kb,
@ -264,7 +262,6 @@ const CardRobotWecomService = ({
<Icon type='icon-jinggao' sx={{ fontSize: 18 }} /> <Icon type='icon-jinggao' sx={{ fontSize: 18 }} />
</Stack> </Stack>
<VersionMask permission={PROFESSION_VERSION_PERMISSION}>
<FormItem <FormItem
label={ label={
<Box> <Box>
@ -297,7 +294,6 @@ const CardRobotWecomService = ({
{...equalKeywordsField} {...equalKeywordsField}
/> />
</FormItem> </FormItem>
</VersionMask>
</> </>
)} )}
</SettingCardItem> </SettingCardItem>

View File

@ -8,7 +8,6 @@ import {
} from '@/request/types'; } from '@/request/types';
import { useAppSelector } from '@/store'; import { useAppSelector } from '@/store';
import { message } from '@ctzhian/ui'; import { message } from '@ctzhian/ui';
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
import { import {
Autocomplete, Autocomplete,
Box, Box,
@ -18,7 +17,7 @@ import {
TextField, TextField,
styled, styled,
} from '@mui/material'; } from '@mui/material';
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { FormItem, SettingCardItem } from './Common'; import { FormItem, SettingCardItem } from './Common';
@ -33,9 +32,15 @@ const WatermarkForm = ({
data?: DomainAppDetailResp; data?: DomainAppDetailResp;
refresh: () => void; refresh: () => void;
}) => { }) => {
const { kb_id } = useAppSelector(state => state.config); const { license, kb_id } = useAppSelector(state => state.config);
const [watermarkIsEdit, setWatermarkIsEdit] = useState(false); const [watermarkIsEdit, setWatermarkIsEdit] = useState(false);
const { control, handleSubmit, setValue, watch } = useForm({ const {
control,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm({
defaultValues: { defaultValues: {
watermark_setting: data?.settings?.watermark_setting ?? null, watermark_setting: data?.settings?.watermark_setting ?? null,
watermark_content: data?.settings?.watermark_content ?? '', watermark_content: data?.settings?.watermark_content ?? '',
@ -43,6 +48,9 @@ const WatermarkForm = ({
}); });
const watermarkSetting = watch('watermark_setting'); const watermarkSetting = watch('watermark_setting');
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]);
const handleSaveWatermark = handleSubmit(values => { const handleSaveWatermark = handleSubmit(values => {
if (!data?.id || values.watermark_setting === null) return; if (!data?.id || values.watermark_setting === null) return;
@ -74,9 +82,8 @@ const WatermarkForm = ({
title='水印' title='水印'
isEdit={watermarkIsEdit} isEdit={watermarkIsEdit}
onSubmit={handleSaveWatermark} onSubmit={handleSaveWatermark}
permission={BUSINESS_VERSION_PERMISSION}
> >
<FormItem label='水印开关'> <FormItem label='水印开关' tooltip={!isEnterprise && '企业版可用'}>
<Controller <Controller
control={control} control={control}
name='watermark_setting' name='watermark_setting'
@ -91,18 +98,18 @@ const WatermarkForm = ({
> >
<FormControlLabel <FormControlLabel
value={ConstsWatermarkSetting.WatermarkVisible} value={ConstsWatermarkSetting.WatermarkVisible}
control={<Radio size='small' />} control={<Radio size='small' disabled={!isEnterprise} />}
label={<StyledRadioLabel></StyledRadioLabel>} label={<StyledRadioLabel></StyledRadioLabel>}
/> />
<FormControlLabel <FormControlLabel
value={ConstsWatermarkSetting.WatermarkHidden} value={ConstsWatermarkSetting.WatermarkHidden}
control={<Radio size='small' />} control={<Radio size='small' disabled={!isEnterprise} />}
label={<StyledRadioLabel></StyledRadioLabel>} label={<StyledRadioLabel></StyledRadioLabel>}
/> />
<FormControlLabel <FormControlLabel
value={ConstsWatermarkSetting.WatermarkDisabled} value={ConstsWatermarkSetting.WatermarkDisabled}
control={<Radio size='small' />} control={<Radio size='small' disabled={!isEnterprise} />}
label={<StyledRadioLabel></StyledRadioLabel>} label={<StyledRadioLabel></StyledRadioLabel>}
/> />
</RadioGroup> </RadioGroup>
@ -121,6 +128,7 @@ const WatermarkForm = ({
placeholder='请输入水印内容, 支持多行输入' placeholder='请输入水印内容, 支持多行输入'
multiline multiline
minRows={2} minRows={2}
disabled={!isEnterprise}
onChange={e => { onChange={e => {
setWatermarkIsEdit(true); setWatermarkIsEdit(true);
field.onChange(e.target.value); field.onChange(e.target.value);
@ -138,6 +146,9 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
const { license } = useAppSelector(state => state.config); const { license } = useAppSelector(state => state.config);
const [questionInputValue, setQuestionInputValue] = useState(''); const [questionInputValue, setQuestionInputValue] = useState('');
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]);
const { control, handleSubmit, setValue } = useForm({ const { control, handleSubmit, setValue } = useForm({
defaultValues: { defaultValues: {
@ -158,18 +169,17 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
}); });
useEffect(() => { useEffect(() => {
if (!kb.id || !BUSINESS_VERSION_PERMISSION.includes(license.edition!)) if (!kb.id || !isEnterprise) return;
return;
getApiProV1Block({ kb_id: kb.id! }).then(res => { getApiProV1Block({ kb_id: kb.id! }).then(res => {
setValue('block_words', res.words || []); setValue('block_words', res.words || []);
}); });
}, [kb, license.edition]); }, [kb, isEnterprise]);
return ( return (
<SettingCardItem title='内容合规' isEdit={isEdit} onSubmit={onSubmit}> <SettingCardItem title='内容合规' isEdit={isEdit} onSubmit={onSubmit}>
<FormItem <FormItem
vertical vertical
permission={BUSINESS_VERSION_PERMISSION} tooltip={!isEnterprise && '企业版可用'}
label='屏蔽 AI 问答中的关键字' label='屏蔽 AI 问答中的关键字'
> >
<Controller <Controller
@ -183,6 +193,7 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
inputValue={questionInputValue} inputValue={questionInputValue}
options={[]} options={[]}
fullWidth fullWidth
disabled={!isEnterprise}
onInputChange={(_, value) => { onInputChange={(_, value) => {
setQuestionInputValue(value); setQuestionInputValue(value);
}} }}
@ -223,14 +234,23 @@ const CopyForm = ({
data?: DomainAppDetailResp; data?: DomainAppDetailResp;
refresh: () => void; refresh: () => void;
}) => { }) => {
const { kb_id } = useAppSelector(state => state.config); const { license, kb_id } = useAppSelector(state => state.config);
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const { control, handleSubmit, setValue } = useForm({ const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm({
defaultValues: { defaultValues: {
copy_setting: data?.settings?.copy_setting ?? null, copy_setting: data?.settings?.copy_setting ?? null,
}, },
}); });
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]);
const handleSaveWatermark = handleSubmit(values => { const handleSaveWatermark = handleSubmit(values => {
if (!data?.id || values.copy_setting === null) return; if (!data?.id || values.copy_setting === null) return;
putApiV1App( putApiV1App(
@ -260,7 +280,7 @@ const CopyForm = ({
isEdit={isEdit} isEdit={isEdit}
onSubmit={handleSaveWatermark} onSubmit={handleSaveWatermark}
> >
<FormItem label='限制复制' permission={BUSINESS_VERSION_PERMISSION}> <FormItem label='限制复制' tooltip={!isEnterprise && '企业版可用'}>
<Controller <Controller
control={control} control={control}
name='copy_setting' name='copy_setting'
@ -275,18 +295,18 @@ const CopyForm = ({
> >
<FormControlLabel <FormControlLabel
value={ConstsCopySetting.CopySettingNone} value={ConstsCopySetting.CopySettingNone}
control={<Radio size='small' />} control={<Radio size='small' disabled={!isEnterprise} />}
label={<StyledRadioLabel></StyledRadioLabel>} label={<StyledRadioLabel></StyledRadioLabel>}
/> />
<FormControlLabel <FormControlLabel
value={ConstsCopySetting.CopySettingAppend} value={ConstsCopySetting.CopySettingAppend}
control={<Radio size='small' />} control={<Radio size='small' disabled={!isEnterprise} />}
label={<StyledRadioLabel></StyledRadioLabel>} label={<StyledRadioLabel></StyledRadioLabel>}
/> />
<FormControlLabel <FormControlLabel
value={ConstsCopySetting.CopySettingDisabled} value={ConstsCopySetting.CopySettingDisabled}
control={<Radio size='small' />} control={<Radio size='small' disabled={!isEnterprise} />}
label={<StyledRadioLabel></StyledRadioLabel>} label={<StyledRadioLabel></StyledRadioLabel>}
/> />
</RadioGroup> </RadioGroup>

View File

@ -26,6 +26,7 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
defaultValues: { defaultValues: {
desc: '', desc: '',
keyword: '', keyword: '',
auto_sitemap: false,
}, },
}); });
@ -43,6 +44,7 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
useEffect(() => { useEffect(() => {
setValue('desc', data.settings?.desc || ''); setValue('desc', data.settings?.desc || '');
setValue('keyword', data.settings?.keyword || ''); setValue('keyword', data.settings?.keyword || '');
setValue('auto_sitemap', data.settings?.auto_sitemap ?? false);
}, [data]); }, [data]);
return ( return (
@ -86,6 +88,25 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
)} )}
/> />
</FormItem> </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> </SettingCardItem>
); );
}; };

View File

@ -1,9 +1,7 @@
import Card from '@/components/Card'; import Card from '@/components/Card';
import { ConstsLicenseEdition } from '@/request/types';
import InfoIcon from '@mui/icons-material/Info'; import InfoIcon from '@mui/icons-material/Info';
import { Button, Stack, styled, SxProps, Tooltip } from '@mui/material'; import { Button, Stack, styled, SxProps, Tooltip } from '@mui/material';
import { createContext, useContext } from 'react'; import { createContext, useContext } from 'react';
import VersionMask from '@/components/VersionMask';
const StyledForm = styled('form')<{ gap?: number | string }>( const StyledForm = styled('form')<{ gap?: number | string }>(
({ theme, gap = 2 }) => ({ ({ theme, gap = 2 }) => ({
@ -42,7 +40,6 @@ const StyledFormLabel = styled('span')<{ required?: boolean }>(
export const StyledFormItem = styled('div')<{ vertical?: boolean }>( export const StyledFormItem = styled('div')<{ vertical?: boolean }>(
({ theme, vertical }) => ({ ({ theme, vertical }) => ({
position: 'relative',
display: 'flex', display: 'flex',
alignItems: vertical ? 'flex-start' : 'center', alignItems: vertical ? 'flex-start' : 'center',
flexDirection: vertical ? 'column' : 'row', flexDirection: vertical ? 'column' : 'row',
@ -85,7 +82,6 @@ export const FormItem = ({
extra, extra,
sx, sx,
labelSx, labelSx,
permission,
}: { }: {
label?: string | React.ReactNode; label?: string | React.ReactNode;
children?: React.ReactNode; children?: React.ReactNode;
@ -96,13 +92,10 @@ export const FormItem = ({
extra?: React.ReactNode; extra?: React.ReactNode;
sx?: SxProps; sx?: SxProps;
labelSx?: SxProps; labelSx?: SxProps;
permission?: number[];
}) => { }) => {
const { vertical: verticalContext, labelWidth: labelWidthContext } = const { vertical: verticalContext, labelWidth: labelWidthContext } =
useContext(FormContext); useContext(FormContext);
return ( return (
<VersionMask permission={permission}>
<StyledFormItem vertical={vertical || verticalContext} sx={sx}> <StyledFormItem vertical={vertical || verticalContext} sx={sx}>
<StyledFormLabelWrapper <StyledFormLabelWrapper
vertical={vertical || verticalContext} vertical={vertical || verticalContext}
@ -113,9 +106,7 @@ export const FormItem = ({
<StyledFormLabel required={required}>{label}</StyledFormLabel> <StyledFormLabel required={required}>{label}</StyledFormLabel>
{tooltip && typeof tooltip === 'string' ? ( {tooltip && typeof tooltip === 'string' ? (
<Tooltip title={tooltip} placement='top' arrow> <Tooltip title={tooltip} placement='top' arrow>
<InfoIcon <InfoIcon sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }} />
sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }}
/>
</Tooltip> </Tooltip>
) : ( ) : (
tooltip tooltip
@ -126,7 +117,6 @@ export const FormItem = ({
</StyledFormLabelWrapper> </StyledFormLabelWrapper>
{children} {children}
</StyledFormItem> </StyledFormItem>
</VersionMask>
); );
}; };
@ -152,7 +142,6 @@ export const SettingCard = ({
}; };
const StyledSettingCardItem = styled('div')(({ theme }) => ({ const StyledSettingCardItem = styled('div')(({ theme }) => ({
position: 'relative',
'&:not(:last-child)': { '&:not(:last-child)': {
borderBottom: `1px solid ${theme.palette.divider}`, borderBottom: `1px solid ${theme.palette.divider}`,
paddingBottom: theme.spacing(4), paddingBottom: theme.spacing(4),
@ -215,12 +204,6 @@ export const SettingCardItem = ({
extra, extra,
more, more,
sx, sx,
permission = [
ConstsLicenseEdition.LicenseEditionFree,
ConstsLicenseEdition.LicenseEditionProfession,
ConstsLicenseEdition.LicenseEditionBusiness,
ConstsLicenseEdition.LicenseEditionEnterprise,
],
}: { }: {
children?: React.ReactNode; children?: React.ReactNode;
title?: React.ReactNode; title?: React.ReactNode;
@ -229,7 +212,6 @@ export const SettingCardItem = ({
extra?: React.ReactNode; extra?: React.ReactNode;
more?: SettingCardItemMore; more?: SettingCardItemMore;
sx?: SxProps; sx?: SxProps;
permission?: number[];
}) => { }) => {
const renderMore = (more: SettingCardItemMore) => { const renderMore = (more: SettingCardItemMore) => {
if (more && typeof more === 'object' && 'type' in more) { if (more && typeof more === 'object' && 'type' in more) {
@ -255,9 +237,7 @@ export const SettingCardItem = ({
return more; return more;
} }
}; };
return ( return (
<VersionMask permission={permission}>
<StyledSettingCardItem sx={sx}> <StyledSettingCardItem sx={sx}>
<StyledSettingCardItemTitleWrapper> <StyledSettingCardItemTitleWrapper>
<StyledSettingCardItemTitle> <StyledSettingCardItemTitle>
@ -272,6 +252,5 @@ export const SettingCardItem = ({
</StyledSettingCardItemTitleWrapper> </StyledSettingCardItemTitleWrapper>
<StyledSettingCardItemContent>{children}</StyledSettingCardItemContent> <StyledSettingCardItemContent>{children}</StyledSettingCardItemContent>
</StyledSettingCardItem> </StyledSettingCardItem>
</VersionMask>
); );
}; };

View File

@ -1,6 +1,9 @@
import { useEffect, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { SettingCardItem } from '../Common'; import { SettingCardItem } from '../Common';
import { Tooltip } from '@mui/material';
import InfoIcon from '@mui/icons-material/Info';
import { Modal, message } from '@ctzhian/ui'; import { Modal, message } from '@ctzhian/ui';
import { Stack, Button } from '@mui/material';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { postApiProV1AuthGroupSync } from '@/request/pro/AuthOrg'; import { postApiProV1AuthGroupSync } from '@/request/pro/AuthOrg';
import { import {
@ -16,7 +19,6 @@ import {
deleteApiProV1AuthGroupDelete, deleteApiProV1AuthGroupDelete,
} from '@/request/pro/AuthGroup'; } from '@/request/pro/AuthGroup';
import GroupTree from './GroupTree'; import GroupTree from './GroupTree';
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
interface UserGroupProps { interface UserGroupProps {
enabled: string; enabled: string;
@ -43,6 +45,10 @@ const UserGroup = ({
GithubComChaitinPandaWikiProApiAuthV1AuthGroupTreeItem[] GithubComChaitinPandaWikiProApiAuthV1AuthGroupTreeItem[]
>([]); >([]);
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]);
const onDeleteUserGroup = (id: number) => { const onDeleteUserGroup = (id: number) => {
Modal.confirm({ Modal.confirm({
title: '删除用户组', title: '删除用户组',
@ -68,15 +74,10 @@ const UserGroup = ({
}); });
}; };
useEffect(() => { useEffect(() => {
if ( if (!kb_id || enabled !== '2' || !isEnterprise) return;
!kb_id ||
enabled !== '2' ||
!BUSINESS_VERSION_PERMISSION.includes(license.edition!)
)
return;
getUserGroup(); getUserGroup();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [kb_id, enabled, license.edition!]); }, [kb_id, enabled, isEnterprise]);
const handleMove = async ({ const handleMove = async ({
id, id,
@ -122,7 +123,32 @@ const UserGroup = ({
}; };
return ( return (
<SettingCardItem title='用户组' permission={BUSINESS_VERSION_PERMISSION}> <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>
// )
// }
>
<Box <Box
sx={{ sx={{
border: '1px dashed', border: '1px dashed',

View File

@ -9,11 +9,6 @@ import QAReferer from './QAReferer';
import RTVisitor from './RTVisitor'; import RTVisitor from './RTVisitor';
import TypeCount from './TypeCount'; import TypeCount from './TypeCount';
import { useAppSelector } from '@/store'; import { useAppSelector } from '@/store';
import { VersionCanUse } from '@/components/VersionMask';
import {
BUSINESS_VERSION_PERMISSION,
PROFESSION_VERSION_PERMISSION,
} from '@/constant/version';
export const TimeList = [ export const TimeList = [
{ label: '近 24 小时', value: 1 }, { label: '近 24 小时', value: 1 },
@ -30,40 +25,13 @@ const Statistic = () => {
const isWideScreen = useMediaQuery('(min-width:1190px)'); const isWideScreen = useMediaQuery('(min-width:1190px)');
const timeList = useMemo(() => { const timeList = useMemo(() => {
const isPro = PROFESSION_VERSION_PERMISSION.includes(license.edition!); const isPro = license.edition === 1 || license.edition === 2;
const isBusiness = BUSINESS_VERSION_PERMISSION.includes(license.edition!); const isEnterprise = license.edition === 2;
return [ return [
{ label: '近 24 小时', value: 1, disabled: false }, { label: '近 24 小时', value: 1, disabled: false },
{ { label: '近 7 天', value: 7, disabled: !isPro },
label: ( { label: '近 30 天', value: 30, disabled: !isEnterprise },
<Stack direction={'row'} alignItems={'center'} gap={0.5}> { label: '近 90 天', value: 90, disabled: !isEnterprise },
<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]); }, [license]);

View File

@ -26,8 +26,6 @@ import {
GetApiV1NodeListParams, GetApiV1NodeListParams,
GetApiV1NodeRecommendNodesParams, GetApiV1NodeRecommendNodesParams,
V1NodeDetailResp, V1NodeDetailResp,
V1NodeRestudyReq,
V1NodeRestudyResp,
} from "./types"; } from "./types";
/** /**
@ -265,38 +263,6 @@ export const getApiV1NodeRecommendNodes = (
...params, ...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 * @description Summary Node
* *

View File

@ -0,0 +1,46 @@
/* 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,
});

View File

@ -10,6 +10,7 @@ export * from './Message'
export * from './Model' export * from './Model'
export * from './Node' export * from './Node'
export * from './NodePermission' export * from './NodePermission'
export * from './NodeRestudy'
export * from './Stat' export * from './Stat'
export * from './User' export * from './User'
export * from './types' export * from './types'

View File

@ -52,12 +52,10 @@ export enum ConstsSourceType {
export enum ConstsLicenseEdition { export enum ConstsLicenseEdition {
/** 开源版 */ /** 开源版 */
LicenseEditionFree = 0, LicenseEditionFree = 0,
/** 专业版 */ /** 联创版 */
LicenseEditionProfession = 1, LicenseEditionContributor = 1,
/** 企业版 */ /** 企业版 */
LicenseEditionEnterprise = 2, LicenseEditionEnterprise = 2,
/** 商业版 */
LicenseEditionBusiness = 3,
} }
export enum ConstsContributeType { export enum ConstsContributeType {
@ -467,7 +465,6 @@ export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp {
} }
export interface GithubComChaitinPandaWikiProApiShareV1AuthWecomReq { export interface GithubComChaitinPandaWikiProApiShareV1AuthWecomReq {
is_app?: boolean;
kb_id?: string; kb_id?: string;
redirect_url?: string; redirect_url?: string;
} }
@ -671,6 +668,8 @@ export interface GetApiProV1TokenListParams {
} }
export interface PostApiV1LicensePayload { export interface PostApiV1LicensePayload {
/** license edition */
license_edition: "contributor" | "enterprise";
/** license type */ /** license type */
license_type: "file" | "code"; license_type: "file" | "code";
/** /**

View File

@ -175,12 +175,10 @@ export enum ConstsNodeAccessPerm {
export enum ConstsLicenseEdition { export enum ConstsLicenseEdition {
/** 开源版 */ /** 开源版 */
LicenseEditionFree = 0, LicenseEditionFree = 0,
/** 专业版 */ /** 联创版 */
LicenseEditionProfession = 1, LicenseEditionContributor = 1,
/** 企业版 */ /** 企业版 */
LicenseEditionEnterprise = 2, LicenseEditionEnterprise = 2,
/** 商业版 */
LicenseEditionBusiness = 3,
} }
export enum ConstsHomePageSetting { export enum ConstsHomePageSetting {
@ -1647,9 +1645,8 @@ export interface V1NodePermissionResp {
} }
export interface V1NodeRestudyReq { export interface V1NodeRestudyReq {
kb_id: string; kb_id?: string;
/** @minItems 1 */ node_ids?: string[];
node_ids: string[];
} }
export type V1NodeRestudyResp = Record<string, any>; export type V1NodeRestudyResp = Record<string, any>;

View File

@ -82,8 +82,6 @@ export interface ConversationItem {
message_id: string; message_id: string;
source: 'history' | 'chat'; source: 'history' | 'chat';
chunk_result: ChunkResultItem[]; chunk_result: ChunkResultItem[];
result_expend: boolean;
thinking_expend: boolean;
thinking_content: string; thinking_content: string;
id: string; id: string;
} }
@ -384,8 +382,6 @@ const AiQaContent: React.FC<{
const solution = await cap.solve(); const solution = await cap.solve();
token = solution.token; token = solution.token;
} catch (error) { } catch (error) {
setLoading(false);
setThinking(4);
message.error('验证失败'); message.error('验证失败');
console.log(error, 'error---------'); console.log(error, 'error---------');
return; return;
@ -469,8 +465,6 @@ const AiQaContent: React.FC<{
if (lastConversation) { if (lastConversation) {
lastConversation.a = answerContent; lastConversation.a = answerContent;
lastConversation.thinking_content = thinkingContent; lastConversation.thinking_content = thinkingContent;
lastConversation.result_expend = false;
lastConversation.thinking_expend = false;
} }
return newConversation; return newConversation;
}); });
@ -519,8 +513,6 @@ const AiQaContent: React.FC<{
source: 'chat', source: 'chat',
chunk_result: [], chunk_result: [],
thinking_content: '', thinking_content: '',
result_expend: true,
thinking_expend: true,
id: uuidv4(), id: uuidv4(),
}); });
messageIdRef.current = ''; messageIdRef.current = '';
@ -639,8 +631,6 @@ const AiQaContent: React.FC<{
source: 'history', source: 'history',
chunk_result: [], chunk_result: [],
thinking_content: '', thinking_content: '',
result_expend: true,
thinking_expend: true,
id: uuidv4(), id: uuidv4(),
}); });
} }
@ -677,8 +667,6 @@ const AiQaContent: React.FC<{
chunk_result: [], chunk_result: [],
thinking_content: '', thinking_content: '',
id: uuidv4(), id: uuidv4(),
result_expend: true,
thinking_expend: true,
}); });
} }
} }
@ -803,16 +791,7 @@ const AiQaContent: React.FC<{
<StyledAiBubble> <StyledAiBubble>
{/* 搜索结果 */} {/* 搜索结果 */}
{item.chunk_result.length > 0 && ( {item.chunk_result.length > 0 && (
<StyledChunkAccordion <StyledChunkAccordion defaultExpanded>
expanded={item.result_expend}
onChange={(event, expanded) => {
setConversation(prev => {
const newConversation = [...prev];
newConversation[index].result_expend = expanded;
return newConversation;
});
}}
>
<StyledChunkAccordionSummary <StyledChunkAccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />} expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
> >
@ -858,16 +837,7 @@ const AiQaContent: React.FC<{
{/* 思考过程 */} {/* 思考过程 */}
{!!item.thinking_content && ( {!!item.thinking_content && (
<StyledThinkingAccordion <StyledThinkingAccordion defaultExpanded>
expanded={item.thinking_expend}
onChange={(event, expanded) => {
setConversation(prev => {
const newConversation = [...prev];
newConversation[index].thinking_expend = expanded;
return newConversation;
});
}}
>
<StyledThinkingAccordionSummary <StyledThinkingAccordionSummary
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />} expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
> >
@ -959,9 +929,6 @@ const AiQaContent: React.FC<{
</> </>
)} )}
</Stack> </Stack>
<Box>
{kbDetail?.settings?.disclaimer_settings?.content}
</Box>
</StyledActionStack> </StyledActionStack>
)} )}
</StyledAiBubble> </StyledAiBubble>

View File

@ -47,9 +47,9 @@ export const StyledUserBubble = styled(Box)(({ theme }) => ({
export const StyledAiBubble = styled(Box)(({ theme }) => ({ export const StyledAiBubble = styled(Box)(({ theme }) => ({
alignSelf: 'flex-start', alignSelf: 'flex-start',
maxWidth: '85%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
width: '100%',
gap: theme.spacing(3), gap: theme.spacing(3),
})); }));

View File

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

View File

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

View File

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

View File

@ -247,9 +247,7 @@ const QaModal: React.FC<QaModalProps> = () => {
<Box <Box
sx={{ sx={{
px: 3, px: 3,
pt: kbDetail?.settings?.web_app_custom_style?.show_brand_info pt: kbDetail?.settings?.disclaimer_settings?.content ? 2 : 0,
? 2
: 0,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@ -265,10 +263,7 @@ const QaModal: React.FC<QaModalProps> = () => {
gap: 1, gap: 1,
}} }}
> >
<Box> <Box>{kbDetail?.settings?.disclaimer_settings?.content}</Box>
{kbDetail?.settings?.web_app_custom_style?.show_brand_info &&
'本网站由 PandaWiki 提供技术支持'}
</Box>
</Typography> </Typography>
</Box> </Box>
</Box> </Box>