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
import (
"math"
"github.com/labstack/echo/v4"
)
@ -12,12 +14,27 @@ type LicenseEdition int32
const (
LicenseEditionFree LicenseEdition = 0 // 开源版
LicenseEditionProfession LicenseEdition = 1 // 专业
LicenseEditionContributor LicenseEdition = 1 // 联创
LicenseEditionEnterprise LicenseEdition = 2 // 企业版
LicenseEditionBusiness LicenseEdition = 3 // 商业版
)
func GetLicenseEdition(c echo.Context) LicenseEdition {
edition, _ := c.Get("edition").(LicenseEdition)
return edition
}
func (e LicenseEdition) GetMaxAuth(sourceType SourceType) int {
switch e {
case LicenseEditionFree:
if sourceType == SourceTypeGitHub {
return 10
}
return 0
case LicenseEditionContributor:
return 10
case LicenseEditionEnterprise:
return math.MaxInt
default:
return 0
}
}

View File

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

View File

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

View File

@ -117,24 +117,20 @@ definitions:
- 0
- 1
- 2
- 3
format: int32
type: integer
x-enum-comments:
LicenseEditionBusiness: 商业
LicenseEditionContributor: 联创
LicenseEditionEnterprise: 企业版
LicenseEditionFree: 开源版
LicenseEditionProfession: 专业版
x-enum-descriptions:
- 开源版
- 专业
- 联创
- 企业版
- 商业版
x-enum-varnames:
- LicenseEditionFree
- LicenseEditionProfession
- LicenseEditionContributor
- LicenseEditionEnterprise
- LicenseEditionBusiness
consts.ModelSettingMode:
enum:
- 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/chaitin/panda-wiki/consts"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/handler"
"github.com/chaitin/panda-wiki/log"
@ -156,7 +157,7 @@ func (h *ShareCommentHandler) GetCommentList(c echo.Context) error {
}
// 查询数据库获取所有评论-->0 所有, 12 为需要审核的评论
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID)
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID, consts.GetLicenseEdition(c))
if err != nil {
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"
"github.com/chaitin/panda-wiki/consts"
"github.com/chaitin/panda-wiki/domain"
)
// KBUserList
@ -56,8 +55,8 @@ func (h *KnowledgeBaseHandler) KBUserInvite(c echo.Context) error {
return h.NewResponseWithError(c, "validate request failed", err)
}
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
}
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)
}
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
}
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)
}
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)
if err != nil {

View File

@ -46,7 +46,7 @@ func NewModelHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *
return handler
}
// GetModelList
// get model list
//
// @Summary get model list
// @Description get model list
@ -66,7 +66,7 @@ func (h *ModelHandler) GetModelList(c echo.Context) error {
return h.NewResponseWithData(c, models)
}
// CreateModel
// create model
//
// @Summary create model
// @Description create model
@ -85,6 +85,9 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
return h.NewResponseWithError(c, "invalid request", err)
}
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
return h.NewResponseWithError(c, "联创版只能使用百智云模型哦~", nil)
}
ctx := c.Request().Context()
param := domain.ModelParam{}
@ -109,7 +112,7 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
return h.NewResponseWithData(c, model)
}
// UpdateModel
// update model
//
// @Description update model
// @Tags model
@ -127,6 +130,9 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
return h.NewResponseWithError(c, "invalid request", err)
}
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
return h.NewResponseWithError(c, "联创版只能使用百智云模型哦~", nil)
}
ctx := c.Request().Context()
if err := h.usecase.Update(ctx, &req); err != nil {
return h.NewResponseWithError(c, "update model failed", err)
@ -134,7 +140,7 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
return h.NewResponseWithData(c, nil)
}
// CheckModel
// check model
//
// @Summary 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 {
return h.NewResponseWithError(c, "validate request body failed", err)
}
req.MaxNode = domain.GetBaseEditionLimitation(ctx).MaxNode
req.MaxNode = 300
if maxNode := c.Get("max_node"); maxNode != nil {
req.MaxNode = maxNode.(int)
}
id, err := h.usecase.Create(c.Request().Context(), req, authInfo.UserId)
if err != nil {
if errors.Is(err, domain.ErrMaxNodeLimitReached) {
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到更高版本", nil)
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到联创版或企业版", nil)
}
return h.NewResponseWithError(c, "create node failed", err)
}

View File

@ -15,7 +15,7 @@ type AuthMiddleware interface {
Authorize(next echo.HandlerFunc) echo.HandlerFunc
ValidateUserRole(role consts.UserRole) echo.MiddlewareFunc
ValidateKBUserPerm(role consts.UserKBPermission) echo.MiddlewareFunc
ValidateLicenseEdition(edition ...consts.LicenseEdition) echo.MiddlewareFunc
ValidateLicenseEdition(edition consts.LicenseEdition) echo.MiddlewareFunc
MustGetUserID(c echo.Context) (string, bool)
}

View File

@ -6,7 +6,6 @@ import (
"encoding/json"
"io"
"net/http"
"slices"
"strings"
"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(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{
Success: false,
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
}
if int(count) >= domain.GetBaseEditionLimitation(ctx).MaxSSOUser {
return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, domain.GetBaseEditionLimitation(ctx).MaxSSOUser)
if int(count) >= licenseEdition.GetMaxAuth(sourceType) {
return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, licenseEdition.GetMaxAuth(sourceType))
}
auth.LastLoginTime = time.Now()

View File

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

View File

@ -344,7 +344,6 @@ func (r *KnowledgeBaseRepository) CreateKnowledgeBase(ctx context.Context, maxKB
Title: kb.Name,
Desc: kb.Name,
Keyword: kb.Name,
AutoSitemap: true,
Icon: domain.DefaultPandaWikiIconB64,
WelcomeStr: fmt.Sprintf("欢迎使用%s", kb.Name),
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.release_id = ?", kbRelease.ID).
Where("nodes.permissions->>'visible' != ?", consts.NodeAccessPermClosed).
Select("node_releases.node_id as id, node_releases.name, node_releases.type, node_releases.parent_id, 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 {
return nil, err
}

View File

@ -60,14 +60,18 @@ func (r *UserRepository) CreateUser(ctx context.Context, user *domain.User, edit
}
user.Password = string(hashedPassword)
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionFree {
var count int64
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
return err
}
if count >= domain.GetBaseEditionLimitation(ctx).MaxAdmin {
return fmt.Errorf("exceed max admin limit, current count: %d, max limit: %d", count, domain.GetBaseEditionLimitation(ctx).MaxAdmin)
if edition == consts.LicenseEditionFree && count >= 1 {
return errors.New("free edition only allows 1 user")
}
if edition == consts.LicenseEditionContributor && count >= 5 {
return errors.New("contributor edition only allows 5 user")
}
}
if err := tx.Create(user).Error; err != nil {
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 {
switch edition {
case consts.LicenseEditionFree:
app, err := u.repo.GetAppDetail(ctx, id)
if err != nil {
return err
}
limitation := domain.GetBaseEditionLimitation(ctx)
if !limitation.AllowCopyProtection && app.Settings.CopySetting != req.Settings.CopySetting {
if app.Settings.WatermarkContent != req.Settings.WatermarkContent ||
app.Settings.WatermarkSetting != req.Settings.WatermarkSetting ||
app.Settings.ContributeSettings != req.Settings.ContributeSettings ||
app.Settings.CopySetting != req.Settings.CopySetting {
return domain.ErrPermissionDenied
}
if !limitation.AllowWatermark {
if app.Settings.WatermarkSetting != req.Settings.WatermarkSetting || app.Settings.WatermarkContent != req.Settings.WatermarkContent {
case consts.LicenseEditionContributor:
app, err := u.repo.GetAppDetail(ctx, id)
if err != nil {
return err
}
if app.Settings.WatermarkContent != req.Settings.WatermarkContent ||
app.Settings.WatermarkSetting != req.Settings.WatermarkSetting ||
app.Settings.CopySetting != req.Settings.CopySetting {
return domain.ErrPermissionDenied
}
case consts.LicenseEditionEnterprise:
return nil
default:
return fmt.Errorf("unsupported license type: %d", edition)
}
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
}
@ -622,8 +618,8 @@ func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId
}
showBrand := true
defaultDisclaimer := "本回答由 PandaWiki 基于 AI 生成,仅供参考。"
if !domain.GetBaseEditionLimitation(ctx).AllowCustomCopyright {
licenseEdition, _ := ctx.Value(consts.ContextKeyEdition).(consts.LicenseEdition)
if licenseEdition < consts.LicenseEditionEnterprise {
appInfo.Settings.WebAppCustomSettings.ShowBrandInfo = &showBrand
appInfo.Settings.DisclaimerSettings.Content = &defaultDisclaimer
} else {

View File

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

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"slices"
"sort"
"strconv"
@ -68,12 +67,12 @@ func (u *StatUseCase) ValidateStatDay(statDay consts.StatDay, edition consts.Lic
case consts.StatDay1:
return nil
case consts.StatDay7:
if edition == consts.LicenseEditionFree {
if edition < consts.LicenseEditionContributor {
return domain.ErrPermissionDenied
}
return nil
case consts.StatDay30, consts.StatDay90:
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) {
if edition < consts.LicenseEditionEnterprise {
return domain.ErrPermissionDenied
}
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();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subscribe, appPreviewData, id]);
}, [subscribe]);
return (
<StyledCommonWrapper>

View File

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

View File

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

View File

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

View File

@ -1,14 +1,24 @@
import HelpCenter from '@/assets/json/help-center.json';
import IconUpgrade from '@/assets/json/upgrade.json';
import LottieIcon from '@/components/LottieIcon';
import { EditionType } from '@/constant/enums';
import { useAppSelector } from '@/store';
import { Box, Stack, Tooltip } from '@mui/material';
import { useEffect, useState } from 'react';
import packageJson from '../../../package.json';
import AuthTypeModal from './AuthTypeModal';
import { 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 versionInfo = useVersionInfo();
const { license } = useAppSelector(state => state.config);
const curVersion = import.meta.env.VITE_APP_VERSION || packageJson.version;
const [latestVersion, setLatestVersion] = useState<string | undefined>(
undefined,
@ -47,8 +57,11 @@ const Version = () => {
>
<Stack direction={'row'} alignItems='center' gap={0.5}>
<Box sx={{ width: 30, color: 'text.tertiary' }}></Box>
<img src={versionInfo.image} style={{ height: 13, marginTop: -1 }} />
{versionInfo.label}
<img
src={versionMap[license.edition!]}
style={{ height: 13, marginTop: -1 }}
/>
{EditionType[license.edition as keyof typeof EditionType].text}
</Stack>
<Stack direction={'row'} gap={0.5}>
<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 { Controller, useForm } from 'react-hook-form';
import { useAppSelector } from '@/store';
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
import { VersionCanUse } from '@/components/VersionMask';
import { ConstsUserKBPermission, V1KBUserInviteReq } from '@/request/types';
import { ConstsLicenseEdition } from '@/request/pro/types';
@ -27,13 +26,9 @@ const VERSION_MAP = {
message: '开源版只支持 1 个管理员',
max: 1,
},
[ConstsLicenseEdition.LicenseEditionProfession]: {
message: '专业版最多支持 20 个管理员',
max: 20,
},
[ConstsLicenseEdition.LicenseEditionBusiness]: {
message: '商业版最多支持 50 个管理员',
max: 50,
[ConstsLicenseEdition.LicenseEditionContributor]: {
message: '联创版最多支持 3 个管理员',
max: 3,
},
};
@ -50,6 +45,9 @@ const MemberAdd = ({
const { kbList, license, refreshAdminRequest } = useAppSelector(
state => state.config,
);
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]);
const {
control,
@ -120,10 +118,6 @@ const MemberAdd = ({
});
});
const isPro = useMemo(() => {
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
}, [license.edition]);
return (
<>
<Button
@ -259,14 +253,6 @@ const MemberAdd = ({
fullWidth
displayEmpty
sx={{ height: 52 }}
MenuProps={{
sx: {
'.Mui-disabled': {
opacity: 1,
color: 'text.disabled',
},
},
}}
renderValue={(value: V1KBUserInviteReq['perm']) => {
return value ? (
PERM_MAP[value]
@ -280,25 +266,17 @@ const MemberAdd = ({
>
</MenuItem>
<MenuItem
disabled={!isEnterprise}
value={ConstsUserKBPermission.UserKBPermissionDocManage}
disabled={!isPro}
>
{' '}
<VersionCanUse
permission={PROFESSION_VERSION_PERMISSION}
/>
{isEnterprise ? '' : '(企业版可用)'}
</MenuItem>
<MenuItem
disabled={!isEnterprise}
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
disabled={!isPro}
>
{' '}
<VersionCanUse
permission={PROFESSION_VERSION_PERMISSION}
/>
{isEnterprise ? '' : '(企业版可用)'}
</MenuItem>
</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: '其他',
};
export const Free = 0;
export const Contributor = 1;
export const Enterprise = 2;
export const EditionType = {
[Free]: {
text: '开源版',
},
[Contributor]: {
text: '联创版',
},
[Enterprise]: {
text: '企业版',
},
};
export const DocWidth = {
full: {
label: '全屏',

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
defaultValues: {
desc: '',
keyword: '',
auto_sitemap: false,
},
});
@ -43,6 +44,7 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
useEffect(() => {
setValue('desc', data.settings?.desc || '');
setValue('keyword', data.settings?.keyword || '');
setValue('auto_sitemap', data.settings?.auto_sitemap ?? false);
}, [data]);
return (
@ -86,6 +88,25 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
)}
/>
</FormItem>
<FormItem label='自动生成 Sitemap'>
<Controller
control={control}
name='auto_sitemap'
render={({ field }) => (
<Checkbox
{...field}
checked={field.value}
size='small'
sx={{ p: 0, m: 0 }}
onChange={event => {
setIsEdit(true);
field.onChange(event);
}}
/>
)}
/>
</FormItem>
</SettingCardItem>
);
};

View File

@ -1,9 +1,7 @@
import Card from '@/components/Card';
import { ConstsLicenseEdition } from '@/request/types';
import InfoIcon from '@mui/icons-material/Info';
import { Button, Stack, styled, SxProps, Tooltip } from '@mui/material';
import { createContext, useContext } from 'react';
import VersionMask from '@/components/VersionMask';
const StyledForm = styled('form')<{ gap?: number | string }>(
({ theme, gap = 2 }) => ({
@ -42,7 +40,6 @@ const StyledFormLabel = styled('span')<{ required?: boolean }>(
export const StyledFormItem = styled('div')<{ vertical?: boolean }>(
({ theme, vertical }) => ({
position: 'relative',
display: 'flex',
alignItems: vertical ? 'flex-start' : 'center',
flexDirection: vertical ? 'column' : 'row',
@ -85,7 +82,6 @@ export const FormItem = ({
extra,
sx,
labelSx,
permission,
}: {
label?: string | React.ReactNode;
children?: React.ReactNode;
@ -96,13 +92,10 @@ export const FormItem = ({
extra?: React.ReactNode;
sx?: SxProps;
labelSx?: SxProps;
permission?: number[];
}) => {
const { vertical: verticalContext, labelWidth: labelWidthContext } =
useContext(FormContext);
return (
<VersionMask permission={permission}>
<StyledFormItem vertical={vertical || verticalContext} sx={sx}>
<StyledFormLabelWrapper
vertical={vertical || verticalContext}
@ -113,9 +106,7 @@ export const FormItem = ({
<StyledFormLabel required={required}>{label}</StyledFormLabel>
{tooltip && typeof tooltip === 'string' ? (
<Tooltip title={tooltip} placement='top' arrow>
<InfoIcon
sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }}
/>
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }} />
</Tooltip>
) : (
tooltip
@ -126,7 +117,6 @@ export const FormItem = ({
</StyledFormLabelWrapper>
{children}
</StyledFormItem>
</VersionMask>
);
};
@ -152,7 +142,6 @@ export const SettingCard = ({
};
const StyledSettingCardItem = styled('div')(({ theme }) => ({
position: 'relative',
'&:not(:last-child)': {
borderBottom: `1px solid ${theme.palette.divider}`,
paddingBottom: theme.spacing(4),
@ -215,12 +204,6 @@ export const SettingCardItem = ({
extra,
more,
sx,
permission = [
ConstsLicenseEdition.LicenseEditionFree,
ConstsLicenseEdition.LicenseEditionProfession,
ConstsLicenseEdition.LicenseEditionBusiness,
ConstsLicenseEdition.LicenseEditionEnterprise,
],
}: {
children?: React.ReactNode;
title?: React.ReactNode;
@ -229,7 +212,6 @@ export const SettingCardItem = ({
extra?: React.ReactNode;
more?: SettingCardItemMore;
sx?: SxProps;
permission?: number[];
}) => {
const renderMore = (more: SettingCardItemMore) => {
if (more && typeof more === 'object' && 'type' in more) {
@ -255,9 +237,7 @@ export const SettingCardItem = ({
return more;
}
};
return (
<VersionMask permission={permission}>
<StyledSettingCardItem sx={sx}>
<StyledSettingCardItemTitleWrapper>
<StyledSettingCardItemTitle>
@ -272,6 +252,5 @@ export const SettingCardItem = ({
</StyledSettingCardItemTitleWrapper>
<StyledSettingCardItemContent>{children}</StyledSettingCardItemContent>
</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 { Tooltip } from '@mui/material';
import InfoIcon from '@mui/icons-material/Info';
import { Modal, message } from '@ctzhian/ui';
import { Stack, Button } from '@mui/material';
import { Box } from '@mui/material';
import { postApiProV1AuthGroupSync } from '@/request/pro/AuthOrg';
import {
@ -16,7 +19,6 @@ import {
deleteApiProV1AuthGroupDelete,
} from '@/request/pro/AuthGroup';
import GroupTree from './GroupTree';
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
interface UserGroupProps {
enabled: string;
@ -43,6 +45,10 @@ const UserGroup = ({
GithubComChaitinPandaWikiProApiAuthV1AuthGroupTreeItem[]
>([]);
const isEnterprise = useMemo(() => {
return license.edition === 2;
}, [license]);
const onDeleteUserGroup = (id: number) => {
Modal.confirm({
title: '删除用户组',
@ -68,15 +74,10 @@ const UserGroup = ({
});
};
useEffect(() => {
if (
!kb_id ||
enabled !== '2' ||
!BUSINESS_VERSION_PERMISSION.includes(license.edition!)
)
return;
if (!kb_id || enabled !== '2' || !isEnterprise) return;
getUserGroup();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [kb_id, enabled, license.edition!]);
}, [kb_id, enabled, isEnterprise]);
const handleMove = async ({
id,
@ -122,7 +123,32 @@ const UserGroup = ({
};
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
sx={{
border: '1px dashed',

View File

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

View File

@ -26,8 +26,6 @@ import {
GetApiV1NodeListParams,
GetApiV1NodeRecommendNodesParams,
V1NodeDetailResp,
V1NodeRestudyReq,
V1NodeRestudyResp,
} from "./types";
/**
@ -265,38 +263,6 @@ export const getApiV1NodeRecommendNodes = (
...params,
});
/**
* @description
*
* @tags Node
* @name PostApiV1NodeRestudy
* @summary
* @request POST:/api/v1/node/restudy
* @secure
* @response `200` `(DomainResponse & {
data?: V1NodeRestudyResp,
})` OK
*/
export const postApiV1NodeRestudy = (
param: V1NodeRestudyReq,
params: RequestParams = {},
) =>
httpRequest<
DomainResponse & {
data?: V1NodeRestudyResp;
}
>({
path: `/api/v1/node/restudy`,
method: "POST",
body: param,
secure: true,
type: ContentType.Json,
format: "json",
...params,
});
/**
* @description Summary Node
*

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 './Node'
export * from './NodePermission'
export * from './NodeRestudy'
export * from './Stat'
export * from './User'
export * from './types'

View File

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

View File

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

View File

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

View File

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

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