mirror of https://github.com/chaitin/PandaWiki.git
Compare commits
25 Commits
cfc53da267
...
b8f2b95f22
| Author | SHA1 | Date |
|---|---|---|
|
|
b8f2b95f22 | |
|
|
98c602819f | |
|
|
be163a5f80 | |
|
|
4c03078103 | |
|
|
2b372fd81d | |
|
|
74c6ba131d | |
|
|
6c77246e34 | |
|
|
d347482d31 | |
|
|
92443dfafe | |
|
|
1dd2d93010 | |
|
|
54bf1fd108 | |
|
|
c23ce398d7 | |
|
|
2c90932f60 | |
|
|
035ce0284d | |
|
|
93559125c2 | |
|
|
6c5cf256ac | |
|
|
23bdfc3d50 | |
|
|
9008c537cd | |
|
|
63a4a964b1 | |
|
|
62f2b2eaf5 | |
|
|
5c1c6368b8 | |
|
|
7282503acf | |
|
|
60a4177229 | |
|
|
2f706a6100 | |
|
|
febcb06654 |
|
|
@ -7,21 +7,22 @@ import (
|
|||
)
|
||||
|
||||
type ShareNodeDetailResp struct {
|
||||
ID string `json:"id"`
|
||||
KbID string `json:"kb_id"`
|
||||
Type domain.NodeType `json:"type"`
|
||||
Status domain.NodeStatus `json:"status"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Meta domain.NodeMeta `json:"meta"`
|
||||
ParentID string `json:"parent_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Permissions domain.NodePermissions `json:"permissions"`
|
||||
CreatorId string `json:"creator_id"`
|
||||
EditorId string `json:"editor_id"`
|
||||
PublisherId string `json:"publisher_id"`
|
||||
CreatorAccount string `json:"creator_account"`
|
||||
EditorAccount string `json:"editor_account"`
|
||||
PublisherAccount string `json:"publisher_account"`
|
||||
ID string `json:"id"`
|
||||
KbID string `json:"kb_id"`
|
||||
Type domain.NodeType `json:"type"`
|
||||
Status domain.NodeStatus `json:"status"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Meta domain.NodeMeta `json:"meta"`
|
||||
ParentID string `json:"parent_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Permissions domain.NodePermissions `json:"permissions"`
|
||||
CreatorId string `json:"creator_id"`
|
||||
EditorId string `json:"editor_id"`
|
||||
PublisherId string `json:"publisher_id"`
|
||||
CreatorAccount string `json:"creator_account"`
|
||||
EditorAccount string `json:"editor_account"`
|
||||
PublisherAccount string `json:"publisher_account"`
|
||||
List []*domain.ShareNodeListItemResp `json:"list" gorm:"-"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//go:build wireinject
|
||||
// +build wireinject
|
||||
|
||||
package main
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//go:build wireinject
|
||||
// +build wireinject
|
||||
|
||||
package main
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//go:build wireinject
|
||||
// +build wireinject
|
||||
|
||||
package main
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package consts
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
|
|
@ -13,28 +11,13 @@ const ContextKeyEdition contextKey = "edition"
|
|||
type LicenseEdition int32
|
||||
|
||||
const (
|
||||
LicenseEditionFree LicenseEdition = 0 // 开源版
|
||||
LicenseEditionContributor LicenseEdition = 1 // 联创版
|
||||
LicenseEditionEnterprise LicenseEdition = 2 // 企业版
|
||||
LicenseEditionFree LicenseEdition = 0 // 开源版
|
||||
LicenseEditionProfession LicenseEdition = 1 // 专业版
|
||||
LicenseEditionEnterprise LicenseEdition = 2 // 企业版
|
||||
LicenseEditionBusiness LicenseEdition = 3 // 商业版
|
||||
)
|
||||
|
||||
func GetLicenseEdition(c echo.Context) LicenseEdition {
|
||||
edition, _ := c.Get("edition").(LicenseEdition)
|
||||
return edition
|
||||
}
|
||||
|
||||
func (e LicenseEdition) GetMaxAuth(sourceType SourceType) int {
|
||||
switch e {
|
||||
case LicenseEditionFree:
|
||||
if sourceType == SourceTypeGitHub {
|
||||
return 10
|
||||
}
|
||||
return 0
|
||||
case LicenseEditionContributor:
|
||||
return 10
|
||||
case LicenseEditionEnterprise:
|
||||
return math.MaxInt
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3478,7 +3478,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"share_chat"
|
||||
"Widget"
|
||||
],
|
||||
"summary": "ChatWidget",
|
||||
"parameters": [
|
||||
|
|
@ -3509,6 +3509,52 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/share/v1/chat/widget/search": {
|
||||
"post": {
|
||||
"description": "WidgetSearch",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Widget"
|
||||
],
|
||||
"summary": "WidgetSearch",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Comment",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChatSearchReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/domain.ChatSearchResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/share/v1/comment": {
|
||||
"post": {
|
||||
"description": "CreateComment",
|
||||
|
|
@ -4067,22 +4113,26 @@ const docTemplate = `{
|
|||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
2,
|
||||
3
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"LicenseEditionContributor": "联创版",
|
||||
"LicenseEditionBusiness": "商业版",
|
||||
"LicenseEditionEnterprise": "企业版",
|
||||
"LicenseEditionFree": "开源版"
|
||||
"LicenseEditionFree": "开源版",
|
||||
"LicenseEditionProfession": "专业版"
|
||||
},
|
||||
"x-enum-descriptions": [
|
||||
"开源版",
|
||||
"联创版",
|
||||
"企业版"
|
||||
"专业版",
|
||||
"企业版",
|
||||
"商业版"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"LicenseEditionFree",
|
||||
"LicenseEditionContributor",
|
||||
"LicenseEditionEnterprise"
|
||||
"LicenseEditionProfession",
|
||||
"LicenseEditionEnterprise",
|
||||
"LicenseEditionBusiness"
|
||||
]
|
||||
},
|
||||
"consts.ModelSettingMode": {
|
||||
|
|
@ -6311,6 +6361,9 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.MessageContent": {
|
||||
"type": "object"
|
||||
},
|
||||
"domain.MessageFrom": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
|
@ -6712,6 +6765,9 @@ const docTemplate = `{
|
|||
"stream": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"stream_options": {
|
||||
"$ref": "#/definitions/domain.OpenAIStreamOptions"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
|
|
@ -6834,7 +6890,7 @@ const docTemplate = `{
|
|||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/domain.MessageContent"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
|
|
@ -6864,6 +6920,14 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.OpenAIStreamOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"include_usage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.OpenAITool": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
@ -7130,6 +7194,35 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.ShareNodeListItemResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"emoji": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/definitions/domain.NodePermissions"
|
||||
},
|
||||
"position": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/domain.NodeType"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.SimpleAuth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -7647,15 +7740,33 @@ const docTemplate = `{
|
|||
"domain.WidgetBotSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"btn_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_logo": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_position": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_style": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_text": {
|
||||
"type": "string"
|
||||
},
|
||||
"disclaimer": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_open": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"modal_position": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"recommend_node_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
@ -7668,6 +7779,9 @@ const docTemplate = `{
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"search_mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme_mode": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
@ -8536,6 +8650,12 @@ const docTemplate = `{
|
|||
"kb_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/domain.ShareNodeListItemResp"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/definitions/domain.NodeMeta"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3471,7 +3471,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"share_chat"
|
||||
"Widget"
|
||||
],
|
||||
"summary": "ChatWidget",
|
||||
"parameters": [
|
||||
|
|
@ -3502,6 +3502,52 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/share/v1/chat/widget/search": {
|
||||
"post": {
|
||||
"description": "WidgetSearch",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Widget"
|
||||
],
|
||||
"summary": "WidgetSearch",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Comment",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChatSearchReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/domain.ChatSearchResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/share/v1/comment": {
|
||||
"post": {
|
||||
"description": "CreateComment",
|
||||
|
|
@ -4060,22 +4106,26 @@
|
|||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
2,
|
||||
3
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"LicenseEditionContributor": "联创版",
|
||||
"LicenseEditionBusiness": "商业版",
|
||||
"LicenseEditionEnterprise": "企业版",
|
||||
"LicenseEditionFree": "开源版"
|
||||
"LicenseEditionFree": "开源版",
|
||||
"LicenseEditionProfession": "专业版"
|
||||
},
|
||||
"x-enum-descriptions": [
|
||||
"开源版",
|
||||
"联创版",
|
||||
"企业版"
|
||||
"专业版",
|
||||
"企业版",
|
||||
"商业版"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"LicenseEditionFree",
|
||||
"LicenseEditionContributor",
|
||||
"LicenseEditionEnterprise"
|
||||
"LicenseEditionProfession",
|
||||
"LicenseEditionEnterprise",
|
||||
"LicenseEditionBusiness"
|
||||
]
|
||||
},
|
||||
"consts.ModelSettingMode": {
|
||||
|
|
@ -6304,6 +6354,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.MessageContent": {
|
||||
"type": "object"
|
||||
},
|
||||
"domain.MessageFrom": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
|
@ -6705,6 +6758,9 @@
|
|||
"stream": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"stream_options": {
|
||||
"$ref": "#/definitions/domain.OpenAIStreamOptions"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
|
|
@ -6827,7 +6883,7 @@
|
|||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/domain.MessageContent"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
|
|
@ -6857,6 +6913,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.OpenAIStreamOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"include_usage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.OpenAITool": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
@ -7123,6 +7187,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.ShareNodeListItemResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"emoji": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/definitions/domain.NodePermissions"
|
||||
},
|
||||
"position": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/domain.NodeType"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.SimpleAuth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -7640,15 +7733,33 @@
|
|||
"domain.WidgetBotSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"btn_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_logo": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_position": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_style": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_text": {
|
||||
"type": "string"
|
||||
},
|
||||
"disclaimer": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_open": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"modal_position": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"recommend_node_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
@ -7661,6 +7772,9 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"search_mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme_mode": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
@ -8529,6 +8643,12 @@
|
|||
"kb_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/domain.ShareNodeListItemResp"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/definitions/domain.NodeMeta"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -117,20 +117,24 @@ definitions:
|
|||
- 0
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
format: int32
|
||||
type: integer
|
||||
x-enum-comments:
|
||||
LicenseEditionContributor: 联创版
|
||||
LicenseEditionBusiness: 商业版
|
||||
LicenseEditionEnterprise: 企业版
|
||||
LicenseEditionFree: 开源版
|
||||
LicenseEditionProfession: 专业版
|
||||
x-enum-descriptions:
|
||||
- 开源版
|
||||
- 联创版
|
||||
- 专业版
|
||||
- 企业版
|
||||
- 商业版
|
||||
x-enum-varnames:
|
||||
- LicenseEditionFree
|
||||
- LicenseEditionContributor
|
||||
- LicenseEditionProfession
|
||||
- LicenseEditionEnterprise
|
||||
- LicenseEditionBusiness
|
||||
consts.ModelSettingMode:
|
||||
enum:
|
||||
- manual
|
||||
|
|
@ -1610,6 +1614,8 @@ definitions:
|
|||
url:
|
||||
type: string
|
||||
type: object
|
||||
domain.MessageContent:
|
||||
type: object
|
||||
domain.MessageFrom:
|
||||
enum:
|
||||
- 1
|
||||
|
|
@ -1871,6 +1877,8 @@ definitions:
|
|||
type: array
|
||||
stream:
|
||||
type: boolean
|
||||
stream_options:
|
||||
$ref: '#/definitions/domain.OpenAIStreamOptions'
|
||||
temperature:
|
||||
type: number
|
||||
tool_choice:
|
||||
|
|
@ -1952,7 +1960,7 @@ definitions:
|
|||
domain.OpenAIMessage:
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
$ref: '#/definitions/domain.MessageContent'
|
||||
name:
|
||||
type: string
|
||||
role:
|
||||
|
|
@ -1973,6 +1981,11 @@ definitions:
|
|||
required:
|
||||
- type
|
||||
type: object
|
||||
domain.OpenAIStreamOptions:
|
||||
properties:
|
||||
include_usage:
|
||||
type: boolean
|
||||
type: object
|
||||
domain.OpenAITool:
|
||||
properties:
|
||||
function:
|
||||
|
|
@ -2146,6 +2159,25 @@ definitions:
|
|||
role:
|
||||
$ref: '#/definitions/schema.RoleType'
|
||||
type: object
|
||||
domain.ShareNodeListItemResp:
|
||||
properties:
|
||||
emoji:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
parent_id:
|
||||
type: string
|
||||
permissions:
|
||||
$ref: '#/definitions/domain.NodePermissions'
|
||||
position:
|
||||
type: number
|
||||
type:
|
||||
$ref: '#/definitions/domain.NodeType'
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
domain.SimpleAuth:
|
||||
properties:
|
||||
enabled:
|
||||
|
|
@ -2488,12 +2520,24 @@ definitions:
|
|||
type: object
|
||||
domain.WidgetBotSettings:
|
||||
properties:
|
||||
btn_id:
|
||||
type: string
|
||||
btn_logo:
|
||||
type: string
|
||||
btn_position:
|
||||
type: string
|
||||
btn_style:
|
||||
type: string
|
||||
btn_text:
|
||||
type: string
|
||||
disclaimer:
|
||||
type: string
|
||||
is_open:
|
||||
type: boolean
|
||||
modal_position:
|
||||
type: string
|
||||
placeholder:
|
||||
type: string
|
||||
recommend_node_ids:
|
||||
items:
|
||||
type: string
|
||||
|
|
@ -2502,6 +2546,8 @@ definitions:
|
|||
items:
|
||||
type: string
|
||||
type: array
|
||||
search_mode:
|
||||
type: string
|
||||
theme_mode:
|
||||
type: string
|
||||
type: object
|
||||
|
|
@ -3075,6 +3121,10 @@ definitions:
|
|||
type: string
|
||||
kb_id:
|
||||
type: string
|
||||
list:
|
||||
items:
|
||||
$ref: '#/definitions/domain.ShareNodeListItemResp'
|
||||
type: array
|
||||
meta:
|
||||
$ref: '#/definitions/domain.NodeMeta'
|
||||
name:
|
||||
|
|
@ -5296,7 +5346,34 @@ paths:
|
|||
$ref: '#/definitions/domain.Response'
|
||||
summary: ChatWidget
|
||||
tags:
|
||||
- share_chat
|
||||
- Widget
|
||||
/share/v1/chat/widget/search:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: WidgetSearch
|
||||
parameters:
|
||||
- description: Comment
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ChatSearchReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/domain.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/domain.ChatSearchResp'
|
||||
type: object
|
||||
summary: WidgetSearch
|
||||
tags:
|
||||
- Widget
|
||||
/share/v1/comment:
|
||||
post:
|
||||
consumes:
|
||||
|
|
|
|||
|
|
@ -405,6 +405,13 @@ type WidgetBotSettings struct {
|
|||
BtnLogo string `json:"btn_logo,omitempty"`
|
||||
RecommendQuestions []string `json:"recommend_questions,omitempty"`
|
||||
RecommendNodeIDs []string `json:"recommend_node_ids,omitempty"`
|
||||
BtnStyle string `json:"btn_style,omitempty"`
|
||||
BtnID string `json:"btn_id,omitempty"`
|
||||
BtnPosition string `json:"btn_position,omitempty"`
|
||||
ModalPosition string `json:"modal_position,omitempty"`
|
||||
SearchMode string `json:"search_mode,omitempty"`
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
Disclaimer string `json:"disclaimer,omitempty"`
|
||||
}
|
||||
|
||||
type BrandGroup struct {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const ContextKeyEditionLimitation contextKey = "edition_limitation"
|
||||
|
||||
type BaseEditionLimitation struct {
|
||||
MaxKb int `json:"max_kb"` // 知识库站点数量
|
||||
MaxNode int `json:"max_node"` // 单个知识库下文档数量
|
||||
MaxSSOUser int `json:"max_sso_users"` // SSO认证用户数量
|
||||
MaxAdmin int64 `json:"max_admin"` // 后台管理员数量
|
||||
AllowAdminPerm bool `json:"allow_admin_perm"` // 支持管理员分权控制
|
||||
AllowCustomCopyright bool `json:"allow_custom_copyright"` // 支持自定义版权信息
|
||||
AllowCommentAudit bool `json:"allow_comment_audit"` // 支持评论审核
|
||||
AllowAdvancedBot bool `json:"allow_advanced_bot"` // 支持高级机器人配置
|
||||
AllowWatermark bool `json:"allow_watermark"` // 支持水印
|
||||
AllowCopyProtection bool `json:"allow_copy_protection"` // 支持内容复制保护
|
||||
AllowOpenAIBotSettings bool `json:"allow_open_ai_bot_settings"` // 支持问答机器人
|
||||
}
|
||||
|
||||
var baseEditionLimitationDefault = BaseEditionLimitation{
|
||||
MaxKb: 1,
|
||||
MaxAdmin: 1,
|
||||
MaxNode: 300,
|
||||
}
|
||||
|
||||
func GetBaseEditionLimitation(c context.Context) BaseEditionLimitation {
|
||||
|
||||
edition, ok := c.Value(ContextKeyEditionLimitation).([]byte)
|
||||
if !ok {
|
||||
return baseEditionLimitationDefault
|
||||
}
|
||||
|
||||
var editionLimitation BaseEditionLimitation
|
||||
if err := json.Unmarshal(edition, &editionLimitation); err != nil {
|
||||
return baseEditionLimitationDefault
|
||||
}
|
||||
|
||||
return editionLimitation
|
||||
}
|
||||
|
|
@ -37,8 +37,14 @@ type MessageContent struct {
|
|||
|
||||
// OpenAIContentPart 表示内容数组中的单个元素
|
||||
type OpenAIContentPart struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL *OpenAIContentPartURL `json:"image_url,omitempty"`
|
||||
}
|
||||
|
||||
// OpenAIContentPartURL represents the image_url field in content parts
|
||||
type OpenAIContentPartURL struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON 自定义解析,支持 string 或 array 格式
|
||||
|
|
@ -63,7 +69,7 @@ func (mc *MessageContent) UnmarshalJSON(data []byte) error {
|
|||
}
|
||||
|
||||
// MarshalJSON 自定义序列化
|
||||
func (mc MessageContent) MarshalJSON() ([]byte, error) {
|
||||
func (mc *MessageContent) MarshalJSON() ([]byte, error) {
|
||||
if mc.isString {
|
||||
return json.Marshal(mc.strValue)
|
||||
}
|
||||
|
|
@ -93,9 +99,9 @@ func (mc *MessageContent) String() string {
|
|||
}
|
||||
// 从数组中提取文本
|
||||
var builder strings.Builder
|
||||
for i, part := range mc.arrValue {
|
||||
for _, part := range mc.arrValue {
|
||||
if part.Type == "text" {
|
||||
if i > 0 && part.Text != "" {
|
||||
if builder.Len() > 0 && part.Text != "" {
|
||||
builder.WriteString(" ")
|
||||
}
|
||||
builder.WriteString(part.Text)
|
||||
|
|
@ -181,9 +187,9 @@ type OpenAIStreamResponse struct {
|
|||
}
|
||||
|
||||
type OpenAIStreamChoice struct {
|
||||
Index int `json:"index"`
|
||||
Delta OpenAIMessage `json:"delta"`
|
||||
FinishReason *string `json:"finish_reason,omitempty"`
|
||||
Index int `json:"index"`
|
||||
Delta OpenAIMessage `json:"delta"`
|
||||
FinishReason *string `json:"finish_reason,omitempty"`
|
||||
}
|
||||
|
||||
// OpenAI 错误响应结构体
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ func NewShareChatHandler(
|
|||
share.POST("/search", h.ChatSearch, h.ShareAuthMiddleware.Authorize)
|
||||
share.POST("/completions", h.ChatCompletions)
|
||||
share.POST("/widget", h.ChatWidget)
|
||||
share.POST("/widget/search", h.WidgetSearch)
|
||||
share.POST("/feedback", h.FeedBack)
|
||||
return h
|
||||
}
|
||||
|
|
@ -131,7 +132,7 @@ func (h *ShareChatHandler) ChatMessage(c echo.Context) error {
|
|||
//
|
||||
// @Summary ChatWidget
|
||||
// @Description ChatWidget
|
||||
// @Tags share_chat
|
||||
// @Tags Widget
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param app_type query string true "app type"
|
||||
|
|
@ -444,7 +445,7 @@ func stringPtr(s string) *string {
|
|||
return &s
|
||||
}
|
||||
|
||||
// ChatMessage chat search
|
||||
// ChatSearch searches chat messages in shared knowledge base
|
||||
//
|
||||
// @Summary ChatSearch
|
||||
// @Description ChatSearch
|
||||
|
|
@ -487,3 +488,43 @@ func (h *ShareChatHandler) ChatSearch(c echo.Context) error {
|
|||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
||||
// WidgetSearch
|
||||
//
|
||||
// @Summary WidgetSearch
|
||||
// @Description WidgetSearch
|
||||
// @Tags Widget
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.ChatSearchReq true "Comment"
|
||||
// @Success 200 {object} domain.Response{data=domain.ChatSearchResp}
|
||||
// @Router /share/v1/chat/widget/search [post]
|
||||
func (h *ShareChatHandler) WidgetSearch(c echo.Context) error {
|
||||
var req domain.ChatSearchReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "parse request failed", err)
|
||||
}
|
||||
req.KBID = c.Request().Header.Get("X-KB-ID")
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// validate widget info
|
||||
widgetAppInfo, err := h.appUsecase.GetWidgetAppInfo(c.Request().Context(), req.KBID)
|
||||
if err != nil {
|
||||
h.logger.Error("get widget app info failed", log.Error(err))
|
||||
return h.sendErrMsg(c, "get app info error")
|
||||
}
|
||||
if !widgetAppInfo.Settings.WidgetBotSettings.IsOpen {
|
||||
return h.sendErrMsg(c, "widget is not open")
|
||||
}
|
||||
|
||||
req.RemoteIP = c.RealIP()
|
||||
|
||||
resp, err := h.chatUsecase.Search(ctx, &req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to search docs", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
|
|
@ -157,7 +156,7 @@ func (h *ShareCommentHandler) GetCommentList(c echo.Context) error {
|
|||
}
|
||||
|
||||
// 查询数据库获取所有评论-->0 所有, 1,2 为需要审核的评论
|
||||
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID, consts.GetLicenseEdition(c))
|
||||
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get comment list", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,5 +91,15 @@ func (h *ShareNodeHandler) GetNodeDetail(c echo.Context) error {
|
|||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get node detail", err)
|
||||
}
|
||||
|
||||
// If the node is a folder, return the list of child nodes
|
||||
if node.Type == domain.NodeTypeFolder {
|
||||
childNodes, err := h.usecase.GetNodeReleaseListByParentID(c.Request().Context(), kbID, id, domain.GetAuthID(c))
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get child nodes", err)
|
||||
}
|
||||
node.List = childNodes
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, node)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
v1 "github.com/chaitin/panda-wiki/api/kb/v1"
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
)
|
||||
|
||||
// KBUserList
|
||||
|
|
@ -55,8 +56,8 @@ func (h *KnowledgeBaseHandler) KBUserInvite(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
|
||||
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
|
||||
}
|
||||
|
||||
err := h.usecase.KBUserInvite(c.Request().Context(), req)
|
||||
|
|
@ -87,8 +88,8 @@ func (h *KnowledgeBaseHandler) KBUserUpdate(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
|
||||
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
|
||||
}
|
||||
|
||||
err := h.usecase.UpdateUserKB(c.Request().Context(), req)
|
||||
|
|
|
|||
|
|
@ -91,11 +91,7 @@ func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "ports is required", nil)
|
||||
}
|
||||
|
||||
req.MaxKB = 1
|
||||
maxKB := c.Get("max_kb")
|
||||
if maxKB != nil {
|
||||
req.MaxKB = maxKB.(int)
|
||||
}
|
||||
req.MaxKB = domain.GetBaseEditionLimitation(c.Request().Context()).MaxKb
|
||||
|
||||
did, err := h.usecase.CreateKnowledgeBase(c.Request().Context(), &req)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func NewModelHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *
|
|||
return handler
|
||||
}
|
||||
|
||||
// get model list
|
||||
// GetModelList
|
||||
//
|
||||
// @Summary get model list
|
||||
// @Description get model list
|
||||
|
|
@ -66,7 +66,7 @@ func (h *ModelHandler) GetModelList(c echo.Context) error {
|
|||
return h.NewResponseWithData(c, models)
|
||||
}
|
||||
|
||||
// create model
|
||||
// CreateModel
|
||||
//
|
||||
// @Summary create model
|
||||
// @Description create model
|
||||
|
|
@ -85,9 +85,6 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
|
||||
return h.NewResponseWithError(c, "联创版只能使用百智云模型哦~", nil)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
|
||||
param := domain.ModelParam{}
|
||||
|
|
@ -112,7 +109,7 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
|
|||
return h.NewResponseWithData(c, model)
|
||||
}
|
||||
|
||||
// update model
|
||||
// UpdateModel
|
||||
//
|
||||
// @Description update model
|
||||
// @Tags model
|
||||
|
|
@ -130,9 +127,6 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
|
||||
return h.NewResponseWithError(c, "联创版只能使用百智云模型哦~", nil)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
if err := h.usecase.Update(ctx, &req); err != nil {
|
||||
return h.NewResponseWithError(c, "update model failed", err)
|
||||
|
|
@ -140,7 +134,7 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
|
|||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// check model
|
||||
// CheckModel
|
||||
//
|
||||
// @Summary check model
|
||||
// @Description check model
|
||||
|
|
|
|||
|
|
@ -81,15 +81,13 @@ func (h *NodeHandler) CreateNode(c echo.Context) error {
|
|||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
req.MaxNode = 300
|
||||
if maxNode := c.Get("max_node"); maxNode != nil {
|
||||
req.MaxNode = maxNode.(int)
|
||||
}
|
||||
|
||||
req.MaxNode = domain.GetBaseEditionLimitation(ctx).MaxNode
|
||||
|
||||
id, err := h.usecase.Create(c.Request().Context(), req, authInfo.UserId)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrMaxNodeLimitReached) {
|
||||
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到联创版或企业版", nil)
|
||||
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到更高版本", nil)
|
||||
}
|
||||
return h.NewResponseWithError(c, "create node failed", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ type AuthMiddleware interface {
|
|||
Authorize(next echo.HandlerFunc) echo.HandlerFunc
|
||||
ValidateUserRole(role consts.UserRole) echo.MiddlewareFunc
|
||||
ValidateKBUserPerm(role consts.UserKBPermission) echo.MiddlewareFunc
|
||||
ValidateLicenseEdition(edition consts.LicenseEdition) echo.MiddlewareFunc
|
||||
ValidateLicenseEdition(edition ...consts.LicenseEdition) echo.MiddlewareFunc
|
||||
MustGetUserID(c echo.Context) (string, bool)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
|
@ -194,7 +195,7 @@ func (m *JWTMiddleware) ValidateKBUserPerm(perm consts.UserKBPermission) echo.Mi
|
|||
}
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) ValidateLicenseEdition(needEdition consts.LicenseEdition) echo.MiddlewareFunc {
|
||||
func (m *JWTMiddleware) ValidateLicenseEdition(needEditions ...consts.LicenseEdition) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
|
||||
|
|
@ -206,7 +207,7 @@ func (m *JWTMiddleware) ValidateLicenseEdition(needEdition consts.LicenseEdition
|
|||
})
|
||||
}
|
||||
|
||||
if edition < needEdition {
|
||||
if !slices.Contains(needEditions, edition) {
|
||||
return c.JSON(http.StatusForbidden, domain.PWResponse{
|
||||
Success: false,
|
||||
Message: "Unauthorized ValidateLicenseEdition",
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit c4dc498df094cb617d31c95580db8239a445d652
|
||||
Subproject commit bb1b17dd5c7d72d40f6a1198b1604f4d3c44116e
|
||||
|
|
@ -300,8 +300,8 @@ func (r *AuthRepo) GetOrCreateAuth(ctx context.Context, auth *domain.Auth, sourc
|
|||
return err
|
||||
}
|
||||
|
||||
if int(count) >= licenseEdition.GetMaxAuth(sourceType) {
|
||||
return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, licenseEdition.GetMaxAuth(sourceType))
|
||||
if int(count) >= domain.GetBaseEditionLimitation(ctx).MaxSSOUser {
|
||||
return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, domain.GetBaseEditionLimitation(ctx).MaxSSOUser)
|
||||
}
|
||||
|
||||
auth.LastLoginTime = time.Now()
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ func (r *CommentRepository) CreateComment(ctx context.Context, comment *domain.C
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string, edition consts.LicenseEdition) ([]*domain.ShareCommentListItem, int64, error) {
|
||||
func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string) ([]*domain.ShareCommentListItem, int64, error) {
|
||||
// 按照时间排序来查询node_id的comments
|
||||
var comments []*domain.ShareCommentListItem
|
||||
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("node_id = ?", nodeID)
|
||||
|
||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
|
||||
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit {
|
||||
query = query.Where("status = ?", domain.CommentStatusAccepted) //accepted
|
||||
}
|
||||
|
||||
|
|
@ -50,14 +50,14 @@ func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string, e
|
|||
|
||||
func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domain.CommentListReq, edition consts.LicenseEdition) ([]*domain.CommentListItem, int64, error) {
|
||||
comments := []*domain.CommentListItem{}
|
||||
query := r.db.Model(&domain.Comment{}).Where("comments.kb_id = ?", req.KbID)
|
||||
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("comments.kb_id = ?", req.KbID)
|
||||
var count int64
|
||||
if req.Status == nil {
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
} else {
|
||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
|
||||
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit {
|
||||
query = query.Where("comments.status = ?", *req.Status)
|
||||
}
|
||||
// 按照时间排序来查询kb_id的comments ->reject pending accepted
|
||||
|
|
@ -84,7 +84,7 @@ func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domai
|
|||
|
||||
func (r *CommentRepository) DeleteCommentList(ctx context.Context, commentID []string) error {
|
||||
// 批量删除指定id的comment,获取删除的总的数量、
|
||||
query := r.db.Model(&domain.Comment{}).Where("id IN (?)", commentID)
|
||||
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("id IN (?)", commentID)
|
||||
|
||||
if err := query.Delete(&domain.Comment{}).Error; err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -341,11 +341,12 @@ func (r *KnowledgeBaseRepository) CreateKnowledgeBase(ctx context.Context, maxKB
|
|||
Name: kb.Name,
|
||||
Type: domain.AppTypeWeb,
|
||||
Settings: domain.AppSettings{
|
||||
Title: kb.Name,
|
||||
Desc: kb.Name,
|
||||
Keyword: kb.Name,
|
||||
Icon: domain.DefaultPandaWikiIconB64,
|
||||
WelcomeStr: fmt.Sprintf("欢迎使用%s", kb.Name),
|
||||
Title: kb.Name,
|
||||
Desc: kb.Name,
|
||||
Keyword: kb.Name,
|
||||
AutoSitemap: true,
|
||||
Icon: domain.DefaultPandaWikiIconB64,
|
||||
WelcomeStr: fmt.Sprintf("欢迎使用%s", kb.Name),
|
||||
Btns: []any{
|
||||
AppBtn{
|
||||
ID: uuid.New().String(),
|
||||
|
|
|
|||
|
|
@ -683,7 +683,7 @@ func (r *NodeRepository) GetNodeReleaseListByKBID(ctx context.Context, kbID stri
|
|||
Where("kb_release_node_releases.kb_id = ?", kbID).
|
||||
Where("kb_release_node_releases.release_id = ?", kbRelease.ID).
|
||||
Where("nodes.permissions->>'visible' != ?", consts.NodeAccessPermClosed).
|
||||
Select("node_releases.node_id as id, node_releases.name, node_releases.type, node_releases.parent_id, node_releases.position, node_releases.meta->>'emoji' as emoji, node_releases.updated_at, nodes.permissions").
|
||||
Select("node_releases.node_id as id, node_releases.name, node_releases.type, node_releases.parent_id, nodes.position, node_releases.meta->>'emoji' as emoji, node_releases.updated_at, nodes.permissions").
|
||||
Find(&nodes).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,18 +60,14 @@ func (r *UserRepository) CreateUser(ctx context.Context, user *domain.User, edit
|
|||
}
|
||||
user.Password = string(hashedPassword)
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionFree {
|
||||
var count int64
|
||||
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if edition == consts.LicenseEditionFree && count >= 1 {
|
||||
return errors.New("free edition only allows 1 user")
|
||||
}
|
||||
if edition == consts.LicenseEditionContributor && count >= 5 {
|
||||
return errors.New("contributor edition only allows 5 user")
|
||||
}
|
||||
var count int64
|
||||
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count >= domain.GetBaseEditionLimitation(ctx).MaxAdmin {
|
||||
return fmt.Errorf("exceed max admin limit, current count: %d, max limit: %d", count, domain.GetBaseEditionLimitation(ctx).MaxAdmin)
|
||||
}
|
||||
|
||||
if err := tx.Create(user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,34 +88,38 @@ func NewAppUsecase(
|
|||
}
|
||||
|
||||
func (u *AppUsecase) ValidateUpdateApp(ctx context.Context, id string, req *domain.UpdateAppReq, edition consts.LicenseEdition) error {
|
||||
switch edition {
|
||||
case consts.LicenseEditionFree:
|
||||
app, err := u.repo.GetAppDetail(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if app.Settings.WatermarkContent != req.Settings.WatermarkContent ||
|
||||
app.Settings.WatermarkSetting != req.Settings.WatermarkSetting ||
|
||||
app.Settings.ContributeSettings != req.Settings.ContributeSettings ||
|
||||
app.Settings.CopySetting != req.Settings.CopySetting {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
case consts.LicenseEditionContributor:
|
||||
app, err := u.repo.GetAppDetail(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if app.Settings.WatermarkContent != req.Settings.WatermarkContent ||
|
||||
app.Settings.WatermarkSetting != req.Settings.WatermarkSetting ||
|
||||
app.Settings.CopySetting != req.Settings.CopySetting {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
case consts.LicenseEditionEnterprise:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported license type: %d", edition)
|
||||
app, err := u.repo.GetAppDetail(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
limitation := domain.GetBaseEditionLimitation(ctx)
|
||||
if !limitation.AllowCopyProtection && app.Settings.CopySetting != req.Settings.CopySetting {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if !limitation.AllowWatermark {
|
||||
if app.Settings.WatermarkSetting != req.Settings.WatermarkSetting || app.Settings.WatermarkContent != req.Settings.WatermarkContent {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
if !limitation.AllowAdvancedBot {
|
||||
if !slices.Equal(app.Settings.WechatServiceContainKeywords, req.Settings.WechatServiceContainKeywords) ||
|
||||
!slices.Equal(app.Settings.WechatServiceEqualKeywords, req.Settings.WechatServiceEqualKeywords) {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
if !limitation.AllowCommentAudit && app.Settings.WebAppCommentSettings.ModerationEnable != req.Settings.WebAppCommentSettings.ModerationEnable {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if !limitation.AllowOpenAIBotSettings {
|
||||
if app.Settings.OpenAIAPIBotSettings.IsEnabled != req.Settings.OpenAIAPIBotSettings.IsEnabled || app.Settings.OpenAIAPIBotSettings.SecretKey != req.Settings.OpenAIAPIBotSettings.SecretKey {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -618,8 +622,8 @@ func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId
|
|||
}
|
||||
showBrand := true
|
||||
defaultDisclaimer := "本回答由 PandaWiki 基于 AI 生成,仅供参考。"
|
||||
licenseEdition, _ := ctx.Value(consts.ContextKeyEdition).(consts.LicenseEdition)
|
||||
if licenseEdition < consts.LicenseEditionEnterprise {
|
||||
|
||||
if !domain.GetBaseEditionLimitation(ctx).AllowCustomCopyright {
|
||||
appInfo.Settings.WebAppCustomSettings.ShowBrandInfo = &showBrand
|
||||
appInfo.Settings.DisclaimerSettings.Content = &defaultDisclaimer
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ func (u *CommentUsecase) CreateComment(ctx context.Context, commentReq *domain.C
|
|||
return CommentStr, nil
|
||||
}
|
||||
|
||||
func (u *CommentUsecase) GetCommentListByNodeID(ctx context.Context, nodeID string, edition consts.LicenseEdition) (*domain.PaginatedResult[[]*domain.ShareCommentListItem], error) {
|
||||
comments, total, err := u.CommentRepo.GetCommentList(ctx, nodeID, edition)
|
||||
func (u *CommentUsecase) GetCommentListByNodeID(ctx context.Context, nodeID string) (*domain.PaginatedResult[[]*domain.ShareCommentListItem], error) {
|
||||
comments, total, err := u.CommentRepo.GetCommentList(ctx, nodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,6 +350,56 @@ func (u *NodeUsecase) GetNodeReleaseListByKBID(ctx context.Context, kbID string,
|
|||
return items, nil
|
||||
}
|
||||
|
||||
func (u *NodeUsecase) GetNodeReleaseListByParentID(ctx context.Context, kbID, parentID string, authId uint) ([]*domain.ShareNodeListItemResp, error) {
|
||||
// 一次性查询所有节点
|
||||
allNodes, err := u.nodeRepo.GetNodeReleaseListByKBID(ctx, kbID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodeGroupIds, err := u.GetNodeIdsByAuthId(ctx, authId, consts.NodePermNameVisible)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 先过滤权限
|
||||
visibleNodes := make([]*domain.ShareNodeListItemResp, 0)
|
||||
for i, node := range allNodes {
|
||||
switch node.Permissions.Visible {
|
||||
case consts.NodeAccessPermOpen:
|
||||
visibleNodes = append(visibleNodes, allNodes[i])
|
||||
case consts.NodeAccessPermPartial:
|
||||
if slices.Contains(nodeGroupIds, node.ID) {
|
||||
visibleNodes = append(visibleNodes, allNodes[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建父子关系映射
|
||||
childrenMap := make(map[string][]*domain.ShareNodeListItemResp)
|
||||
for _, node := range visibleNodes {
|
||||
childrenMap[node.ParentID] = append(childrenMap[node.ParentID], node)
|
||||
}
|
||||
|
||||
// 递归收集所有后代节点
|
||||
result := make([]*domain.ShareNodeListItemResp, 0)
|
||||
u.collectDescendants(parentID, childrenMap, &result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// collectDescendants 递归收集所有后代节点
|
||||
func (u *NodeUsecase) collectDescendants(parentID string, childrenMap map[string][]*domain.ShareNodeListItemResp, result *[]*domain.ShareNodeListItemResp) {
|
||||
children := childrenMap[parentID]
|
||||
for _, child := range children {
|
||||
*result = append(*result, child)
|
||||
// 如果是文件夹,递归收集其子节点
|
||||
if child.Type == domain.NodeTypeFolder {
|
||||
u.collectDescendants(child.ID, childrenMap, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *NodeUsecase) GetNodeIdsByAuthId(ctx context.Context, authId uint, PermName consts.NodePermName) ([]string, error) {
|
||||
authGroups, err := u.authRepo.GetAuthGroupWithParentsByAuthId(ctx, authId)
|
||||
if err != nil {
|
||||
|
|
@ -407,7 +457,7 @@ func (u *NodeUsecase) GetNodePermissionsByID(ctx context.Context, id, kbID strin
|
|||
}
|
||||
|
||||
func (u *NodeUsecase) ValidateNodePermissionsEdit(req v1.NodePermissionEditReq, edition consts.LicenseEdition) error {
|
||||
if edition != consts.LicenseEditionEnterprise {
|
||||
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) {
|
||||
if req.Permissions.Answerable == consts.NodeAccessPermPartial || req.Permissions.Visitable == consts.NodeAccessPermPartial || req.Permissions.Visible == consts.NodeAccessPermPartial {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
|
|
@ -67,12 +68,12 @@ func (u *StatUseCase) ValidateStatDay(statDay consts.StatDay, edition consts.Lic
|
|||
case consts.StatDay1:
|
||||
return nil
|
||||
case consts.StatDay7:
|
||||
if edition < consts.LicenseEditionContributor {
|
||||
if edition == consts.LicenseEditionFree {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
case consts.StatDay30, consts.StatDay90:
|
||||
if edition < consts.LicenseEditionEnterprise {
|
||||
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -588,6 +588,7 @@ export type ChatConversationItem = {
|
|||
export type ChatConversationPair = {
|
||||
user: string;
|
||||
assistant: string;
|
||||
thinking_content: string;
|
||||
created_at: string;
|
||||
info: {
|
||||
feedback_content: string;
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
File diff suppressed because one or more lines are too long
|
|
@ -144,7 +144,7 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
|
|||
callback();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [subscribe]);
|
||||
}, [subscribe, appPreviewData, id]);
|
||||
|
||||
return (
|
||||
<StyledCommonWrapper>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { setAppPreviewData } from '@/store/slices/config';
|
|||
import { DomainSocialMediaAccount } from '@/request/types';
|
||||
import Switch from '../basicComponents/Switch';
|
||||
import DragSocialInfo from '../basicComponents/DragSocialInfo';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface FooterConfigProps {
|
||||
data?: AppDetail | null;
|
||||
|
|
@ -75,9 +77,6 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
);
|
||||
const footer_show_intro = watch('footer_show_intro');
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
useEffect(() => {
|
||||
if (isEdit && appPreviewData) {
|
||||
setValue(
|
||||
|
|
@ -506,29 +505,33 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
)}
|
||||
/>
|
||||
</Stack>
|
||||
{isEnterprise && (
|
||||
<Stack direction={'column'} gap={2}>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
lineHeight: '22px',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 600,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: 4,
|
||||
height: 12,
|
||||
bgcolor: '#3248F2',
|
||||
borderRadius: '2px',
|
||||
mr: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
PandaWiki 版权信息
|
||||
</Box>
|
||||
|
||||
<Stack direction={'column'} gap={2}>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
lineHeight: '22px',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 600,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: 4,
|
||||
height: 12,
|
||||
bgcolor: '#3248F2',
|
||||
borderRadius: '2px',
|
||||
mr: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
PandaWiki 版权信息
|
||||
</Box>
|
||||
<VersionMask
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
sx={{ inset: '-8px 0' }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='show_brand_info'
|
||||
|
|
@ -548,7 +551,6 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
<Switch
|
||||
sx={{ marginLeft: 'auto' }}
|
||||
{...field}
|
||||
disabled={!isEnterprise}
|
||||
checked={field?.value === false ? false : true}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.checked);
|
||||
|
|
@ -558,8 +560,8 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
</Stack>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</VersionMask>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { KnowledgeBaseListItem } from '@/api';
|
||||
import { useURLSearchParams } from '@/hooks';
|
||||
import { useFeatureValue } from '@/hooks';
|
||||
import { ConstsUserRole } from '@/request/types';
|
||||
import { useAppDispatch, useAppSelector } from '@/store';
|
||||
import { setKbC, setKbId } from '@/store/slices/config';
|
||||
|
|
@ -23,14 +24,14 @@ const KBSelect = () => {
|
|||
|
||||
const dispatch = useAppDispatch();
|
||||
const [_, setSearchParams] = useURLSearchParams();
|
||||
const { kb_id, kbList, license, user } = useAppSelector(
|
||||
state => state.config,
|
||||
);
|
||||
const { kb_id, kbList, user } = useAppSelector(state => state.config);
|
||||
|
||||
const [modifyOpen, setModifyOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [opraData, setOpraData] = useState<KnowledgeBaseListItem | null>(null);
|
||||
|
||||
const wikiCount = useFeatureValue('wikiCount');
|
||||
|
||||
return (
|
||||
<>
|
||||
{(kbList || []).length > 0 && (
|
||||
|
|
@ -121,8 +122,7 @@ const KBSelect = () => {
|
|||
}}
|
||||
fullWidth
|
||||
disabled={
|
||||
(license.edition === 0 && (kbList || []).length >= 1) ||
|
||||
(license.edition === 1 && (kbList || []).length >= 3) ||
|
||||
(kbList || []).length >= wikiCount ||
|
||||
user.role === ConstsUserRole.UserRoleUser
|
||||
}
|
||||
onClick={event => {
|
||||
|
|
|
|||
|
|
@ -3,27 +3,20 @@ import {
|
|||
getApiV1License,
|
||||
deleteApiV1License,
|
||||
} from '@/request/pro/License';
|
||||
import { PostApiV1LicensePayload } from '@/request/pro/types';
|
||||
import HelpCenter from '@/assets/json/help-center.json';
|
||||
import Takeoff from '@/assets/json/takeoff.json';
|
||||
import error from '@/assets/json/error.json';
|
||||
import IconUpgrade from '@/assets/json/upgrade.json';
|
||||
import Upload from '@/components/UploadFile/Drag';
|
||||
import { EditionType } from '@/constant/enums';
|
||||
import { useVersionInfo } from '@/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '@/store';
|
||||
import { setLicense } from '@/store/slices/config';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Stack,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { Box, Button, IconButton, Stack, TextField } from '@mui/material';
|
||||
import { CusTabs, Icon, message, Modal } from '@ctzhian/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { useState } from 'react';
|
||||
import LottieIcon from '../LottieIcon';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
|
||||
interface AuthTypeModalProps {
|
||||
open: boolean;
|
||||
|
|
@ -42,10 +35,9 @@ const AuthTypeModal = ({
|
|||
const { license } = useAppSelector(state => state.config);
|
||||
|
||||
const [selected, setSelected] = useState<'file' | 'code'>(
|
||||
license.edition === 2 ? 'file' : 'code',
|
||||
);
|
||||
const [authVersion, setAuthVersion] = useState<'contributor' | 'enterprise'>(
|
||||
license.edition === 2 ? 'enterprise' : 'contributor',
|
||||
license.edition === ConstsLicenseEdition.LicenseEditionEnterprise
|
||||
? 'file'
|
||||
: 'code',
|
||||
);
|
||||
const [updateOpen, setUpdateOpen] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
|
|
@ -53,16 +45,15 @@ const AuthTypeModal = ({
|
|||
const [file, setFile] = useState<File | undefined>(undefined);
|
||||
const [unbindLoading, setUnbindLoading] = useState(false);
|
||||
|
||||
const versionInfo = useVersionInfo();
|
||||
|
||||
const handleSubmit = () => {
|
||||
const params: PostApiV1LicensePayload = {
|
||||
license_edition: authVersion,
|
||||
setLoading(true);
|
||||
postApiV1License({
|
||||
license_type: selected,
|
||||
license_code: code,
|
||||
license_file: file,
|
||||
};
|
||||
setLoading(true);
|
||||
|
||||
postApiV1License(params)
|
||||
})
|
||||
.then(() => {
|
||||
message.success('激活成功');
|
||||
setUpdateOpen(false);
|
||||
|
|
@ -148,10 +139,8 @@ const AuthTypeModal = ({
|
|||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<Box sx={{ width: 120, flexShrink: 0 }}>产品型号</Box>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={2}>
|
||||
<Box sx={{ minWidth: 50 }}>
|
||||
{EditionType[license.edition as keyof typeof EditionType].text}
|
||||
</Box>
|
||||
{license.edition === 0 ? (
|
||||
<Box sx={{ minWidth: 50 }}>{versionInfo.label}</Box>
|
||||
{license.edition === ConstsLicenseEdition.LicenseEditionFree ? (
|
||||
<Stack direction={'row'} gap={2}>
|
||||
<Button
|
||||
size='small'
|
||||
|
|
@ -240,7 +229,7 @@ const AuthTypeModal = ({
|
|||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
{license.edition! > 0 && (
|
||||
{license.edition! !== ConstsLicenseEdition.LicenseEditionFree && (
|
||||
<Box>
|
||||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<Box sx={{ width: 120, flexShrink: 0 }}>授权时间</Box>
|
||||
|
|
@ -288,18 +277,6 @@ const AuthTypeModal = ({
|
|||
value={selected}
|
||||
change={(v: string) => setSelected(v as 'file' | 'code')}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
sx={{ mt: 2 }}
|
||||
value={authVersion}
|
||||
onChange={e =>
|
||||
setAuthVersion(e.target.value as 'contributor' | 'enterprise')
|
||||
}
|
||||
>
|
||||
<MenuItem value='contributor'>联创版</MenuItem>
|
||||
<MenuItem value='enterprise'>企业版</MenuItem>
|
||||
</TextField>
|
||||
{selected === 'code' && (
|
||||
<TextField
|
||||
sx={{ mt: 2 }}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,14 @@
|
|||
import HelpCenter from '@/assets/json/help-center.json';
|
||||
import IconUpgrade from '@/assets/json/upgrade.json';
|
||||
import LottieIcon from '@/components/LottieIcon';
|
||||
import { EditionType } from '@/constant/enums';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { Box, Stack, Tooltip } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import packageJson from '../../../package.json';
|
||||
import AuthTypeModal from './AuthTypeModal';
|
||||
import freeVersion from '@/assets/images/free-version.png';
|
||||
import enterpriseVersion from '@/assets/images/enterprise-version.png';
|
||||
import contributorVersion from '@/assets/images/contributor-version.png';
|
||||
|
||||
const versionMap = {
|
||||
0: freeVersion,
|
||||
1: contributorVersion,
|
||||
2: enterpriseVersion,
|
||||
};
|
||||
import { useVersionInfo } from '@/hooks';
|
||||
|
||||
const Version = () => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
const versionInfo = useVersionInfo();
|
||||
const curVersion = import.meta.env.VITE_APP_VERSION || packageJson.version;
|
||||
const [latestVersion, setLatestVersion] = useState<string | undefined>(
|
||||
undefined,
|
||||
|
|
@ -57,11 +47,8 @@ const Version = () => {
|
|||
>
|
||||
<Stack direction={'row'} alignItems='center' gap={0.5}>
|
||||
<Box sx={{ width: 30, color: 'text.tertiary' }}>型号</Box>
|
||||
<img
|
||||
src={versionMap[license.edition!]}
|
||||
style={{ height: 13, marginTop: -1 }}
|
||||
/>
|
||||
{EditionType[license.edition as keyof typeof EditionType].text}
|
||||
<img src={versionInfo.image} style={{ height: 13, marginTop: -1 }} />
|
||||
{versionInfo.label}
|
||||
</Stack>
|
||||
<Stack direction={'row'} gap={0.5}>
|
||||
<Box sx={{ width: 30, color: 'text.tertiary' }}>版本</Box>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import { Modal, message } from '@ctzhian/ui';
|
|||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import { ConstsUserKBPermission, V1KBUserInviteReq } from '@/request/types';
|
||||
import { ConstsLicenseEdition } from '@/request/pro/types';
|
||||
|
||||
|
|
@ -26,9 +27,13 @@ const VERSION_MAP = {
|
|||
message: '开源版只支持 1 个管理员',
|
||||
max: 1,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionContributor]: {
|
||||
message: '联创版最多支持 3 个管理员',
|
||||
max: 3,
|
||||
[ConstsLicenseEdition.LicenseEditionProfession]: {
|
||||
message: '专业版最多支持 20 个管理员',
|
||||
max: 20,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionBusiness]: {
|
||||
message: '商业版最多支持 50 个管理员',
|
||||
max: 50,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -45,9 +50,6 @@ const MemberAdd = ({
|
|||
const { kbList, license, refreshAdminRequest } = useAppSelector(
|
||||
state => state.config,
|
||||
);
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const {
|
||||
control,
|
||||
|
|
@ -118,6 +120,10 @@ const MemberAdd = ({
|
|||
});
|
||||
});
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
|
@ -253,6 +259,14 @@ const MemberAdd = ({
|
|||
fullWidth
|
||||
displayEmpty
|
||||
sx={{ height: 52 }}
|
||||
MenuProps={{
|
||||
sx: {
|
||||
'.Mui-disabled': {
|
||||
opacity: '1 !important',
|
||||
color: 'text.disabled',
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderValue={(value: V1KBUserInviteReq['perm']) => {
|
||||
return value ? (
|
||||
PERM_MAP[value]
|
||||
|
|
@ -266,17 +280,25 @@ const MemberAdd = ({
|
|||
>
|
||||
完全控制
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||
disabled={!isPro}
|
||||
>
|
||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
||||
文档管理{' '}
|
||||
<VersionCanUse
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
/>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||
disabled={!isPro}
|
||||
>
|
||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
||||
数据运营{' '}
|
||||
<VersionCanUse
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
/>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
import { styled } from '@mui/material';
|
||||
import { useVersionInfo } from '@/hooks';
|
||||
import { VersionInfoMap } from '@/constant/version';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
import { SxProps } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const StyledMaskWrapper = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const StyledMask = styled('div')(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
inset: -8,
|
||||
zIndex: 99,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flex: 1,
|
||||
borderRadius: '10px',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
background: 'rgba(241,242,248,0.8)',
|
||||
backdropFilter: 'blur(0.5px)',
|
||||
}));
|
||||
|
||||
const StyledMaskContent = styled('div')(({ theme }) => ({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
const StyledMaskVersion = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(0.5),
|
||||
padding: theme.spacing(0.5, 1),
|
||||
backgroundColor: theme.palette.background.paper3,
|
||||
borderRadius: '10px',
|
||||
fontSize: 12,
|
||||
lineHeight: 1,
|
||||
color: theme.palette.light.main,
|
||||
}));
|
||||
|
||||
const VersionMask = ({
|
||||
permission = [
|
||||
ConstsLicenseEdition.LicenseEditionFree,
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
],
|
||||
children,
|
||||
sx,
|
||||
}: {
|
||||
permission?: ConstsLicenseEdition[];
|
||||
children?: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
const versionInfo = useVersionInfo();
|
||||
const hasPermission = permission.includes(versionInfo.permission);
|
||||
if (hasPermission) return children;
|
||||
const nextVersionInfo = VersionInfoMap[permission[0]];
|
||||
|
||||
return (
|
||||
<StyledMaskWrapper>
|
||||
{children}
|
||||
<StyledMask sx={sx}>
|
||||
<StyledMaskContent>
|
||||
<StyledMaskVersion sx={{ backgroundColor: nextVersionInfo.bgColor }}>
|
||||
<img
|
||||
src={nextVersionInfo.image}
|
||||
style={{ width: 12, objectFit: 'contain', marginTop: 1 }}
|
||||
alt={nextVersionInfo.label}
|
||||
/>
|
||||
{nextVersionInfo?.label}可用
|
||||
</StyledMaskVersion>
|
||||
</StyledMaskContent>
|
||||
</StyledMask>
|
||||
</StyledMaskWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const VersionCanUse = ({
|
||||
permission = [
|
||||
ConstsLicenseEdition.LicenseEditionFree,
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
],
|
||||
sx,
|
||||
}: {
|
||||
permission?: ConstsLicenseEdition[];
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
const versionInfo = useVersionInfo();
|
||||
const hasPermission = permission.includes(versionInfo.permission);
|
||||
if (hasPermission) return null;
|
||||
const nextVersionInfo = VersionInfoMap[permission[0]];
|
||||
return (
|
||||
<StyledMaskContent sx={{ width: 'auto', ml: 1, ...sx }}>
|
||||
<StyledMaskVersion sx={{ backgroundColor: nextVersionInfo.bgColor }}>
|
||||
<img
|
||||
src={nextVersionInfo.image}
|
||||
style={{ width: 12, objectFit: 'contain', marginTop: 1 }}
|
||||
alt={nextVersionInfo.label}
|
||||
/>
|
||||
{nextVersionInfo?.label}可用
|
||||
</StyledMaskVersion>
|
||||
</StyledMaskContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default VersionMask;
|
||||
|
|
@ -797,21 +797,6 @@ export const FeedbackType = {
|
|||
3: '其他',
|
||||
};
|
||||
|
||||
export const Free = 0;
|
||||
export const Contributor = 1;
|
||||
export const Enterprise = 2;
|
||||
export const EditionType = {
|
||||
[Free]: {
|
||||
text: '开源版',
|
||||
},
|
||||
[Contributor]: {
|
||||
text: '联创版',
|
||||
},
|
||||
[Enterprise]: {
|
||||
text: '企业版',
|
||||
},
|
||||
};
|
||||
|
||||
export const DocWidth = {
|
||||
full: {
|
||||
label: '全屏',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,293 @@
|
|||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
|
||||
import freeVersion from '@/assets/images/free-version.png';
|
||||
import proVersion from '@/assets/images/pro-version.png';
|
||||
import businessVersion from '@/assets/images/business-version.png';
|
||||
import enterpriseVersion from '@/assets/images/enterprise-version.png';
|
||||
|
||||
export const PROFESSION_VERSION_PERMISSION = [
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
];
|
||||
|
||||
export const BUSINESS_VERSION_PERMISSION = [
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
];
|
||||
|
||||
export const ENTERPRISE_VERSION_PERMISSION = [
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
];
|
||||
|
||||
export const VersionInfoMap = {
|
||||
[ConstsLicenseEdition.LicenseEditionFree]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionFree,
|
||||
label: '开源版',
|
||||
image: freeVersion,
|
||||
bgColor: '#8E9DAC',
|
||||
nextVersion: ConstsLicenseEdition.LicenseEditionProfession,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionProfession]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionProfession,
|
||||
label: '专业版',
|
||||
image: proVersion,
|
||||
bgColor: '#0933BA',
|
||||
nextVersion: ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionBusiness]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
label: '商业版',
|
||||
image: businessVersion,
|
||||
bgColor: '#382A79',
|
||||
nextVersion: ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionEnterprise]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
label: '企业版',
|
||||
image: enterpriseVersion,
|
||||
bgColor: '#21222D',
|
||||
nextVersion: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 功能支持状态
|
||||
*/
|
||||
export enum FeatureStatus {
|
||||
/** 不支持 */
|
||||
NOT_SUPPORTED = 'not_supported',
|
||||
/** 支持 */
|
||||
SUPPORTED = 'supported',
|
||||
/** 基础配置 */
|
||||
BASIC = 'basic',
|
||||
/** 高级配置 */
|
||||
ADVANCED = 'advanced',
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本信息配置
|
||||
*/
|
||||
export interface VersionInfo {
|
||||
/** 版本名称 */
|
||||
label: string;
|
||||
/** 功能特性 */
|
||||
features: {
|
||||
/** Wiki 站点数量 */
|
||||
wikiCount: number;
|
||||
/** 每个 Wiki 的文档数量 */
|
||||
docCountPerWiki: number;
|
||||
/** 管理员数量 */
|
||||
adminCount: number;
|
||||
/** 管理员分权控制 */
|
||||
adminPermissionControl: FeatureStatus;
|
||||
/** SEO 配置 */
|
||||
seoConfig: FeatureStatus;
|
||||
/** 多语言支持 */
|
||||
multiLanguage: FeatureStatus;
|
||||
/** 自定义版权信息 */
|
||||
customCopyright: FeatureStatus;
|
||||
/** 访问流量分析 */
|
||||
trafficAnalysis: FeatureStatus;
|
||||
/** 自定义 AI 提示词 */
|
||||
customAIPrompt: FeatureStatus;
|
||||
/** SSO 登录 */
|
||||
ssoLogin: number;
|
||||
/** 访客权限控制 */
|
||||
visitorPermissionControl: FeatureStatus;
|
||||
/** 页面水印 */
|
||||
pageWatermark: FeatureStatus;
|
||||
/** 内容不可复制 */
|
||||
contentNoCopy: FeatureStatus;
|
||||
/** 敏感内容过滤 */
|
||||
sensitiveContentFilter: FeatureStatus;
|
||||
/** 网页挂件机器人 */
|
||||
webWidgetRobot: FeatureStatus;
|
||||
/** 飞书问答机器人 */
|
||||
feishuQARobot: FeatureStatus;
|
||||
/** 钉钉问答机器人 */
|
||||
dingtalkQARobot: FeatureStatus;
|
||||
/** 企业微信问答机器人 */
|
||||
wecomQARobot: FeatureStatus;
|
||||
/** 企业微信客服机器人 */
|
||||
wecomServiceRobot: FeatureStatus;
|
||||
/** Discord 问答机器人 */
|
||||
discordQARobot: FeatureStatus;
|
||||
/** 文档历史版本管理 */
|
||||
docVersionHistory: FeatureStatus;
|
||||
/** API 调用 */
|
||||
apiCall: FeatureStatus;
|
||||
/** 项目源码 */
|
||||
sourceCode: FeatureStatus;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本信息映射
|
||||
*/
|
||||
export const VERSION_INFO: Record<ConstsLicenseEdition, VersionInfo> = {
|
||||
[ConstsLicenseEdition.LicenseEditionFree]: {
|
||||
label: '开源版',
|
||||
features: {
|
||||
wikiCount: 1,
|
||||
docCountPerWiki: 300,
|
||||
adminCount: 1,
|
||||
adminPermissionControl: FeatureStatus.NOT_SUPPORTED,
|
||||
seoConfig: FeatureStatus.BASIC,
|
||||
multiLanguage: FeatureStatus.NOT_SUPPORTED,
|
||||
customCopyright: FeatureStatus.NOT_SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.BASIC,
|
||||
customAIPrompt: FeatureStatus.NOT_SUPPORTED,
|
||||
ssoLogin: 0,
|
||||
visitorPermissionControl: FeatureStatus.NOT_SUPPORTED,
|
||||
pageWatermark: FeatureStatus.NOT_SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.NOT_SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.NOT_SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.BASIC,
|
||||
feishuQARobot: FeatureStatus.BASIC,
|
||||
dingtalkQARobot: FeatureStatus.BASIC,
|
||||
wecomQARobot: FeatureStatus.BASIC,
|
||||
wecomServiceRobot: FeatureStatus.BASIC,
|
||||
discordQARobot: FeatureStatus.BASIC,
|
||||
docVersionHistory: FeatureStatus.NOT_SUPPORTED,
|
||||
apiCall: FeatureStatus.NOT_SUPPORTED,
|
||||
sourceCode: FeatureStatus.SUPPORTED,
|
||||
},
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionProfession]: {
|
||||
label: '专业版',
|
||||
features: {
|
||||
wikiCount: 10,
|
||||
docCountPerWiki: 10000,
|
||||
adminCount: 20,
|
||||
adminPermissionControl: FeatureStatus.SUPPORTED,
|
||||
seoConfig: FeatureStatus.ADVANCED,
|
||||
multiLanguage: FeatureStatus.SUPPORTED,
|
||||
customCopyright: FeatureStatus.SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.ADVANCED,
|
||||
customAIPrompt: FeatureStatus.SUPPORTED,
|
||||
ssoLogin: 0,
|
||||
visitorPermissionControl: FeatureStatus.NOT_SUPPORTED,
|
||||
pageWatermark: FeatureStatus.NOT_SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.NOT_SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.NOT_SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.ADVANCED,
|
||||
feishuQARobot: FeatureStatus.ADVANCED,
|
||||
dingtalkQARobot: FeatureStatus.ADVANCED,
|
||||
wecomQARobot: FeatureStatus.ADVANCED,
|
||||
wecomServiceRobot: FeatureStatus.ADVANCED,
|
||||
discordQARobot: FeatureStatus.ADVANCED,
|
||||
docVersionHistory: FeatureStatus.NOT_SUPPORTED,
|
||||
apiCall: FeatureStatus.NOT_SUPPORTED,
|
||||
sourceCode: FeatureStatus.NOT_SUPPORTED,
|
||||
},
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionBusiness]: {
|
||||
label: '商业版',
|
||||
features: {
|
||||
wikiCount: 20,
|
||||
docCountPerWiki: 10000,
|
||||
adminCount: 50,
|
||||
adminPermissionControl: FeatureStatus.SUPPORTED,
|
||||
seoConfig: FeatureStatus.ADVANCED,
|
||||
multiLanguage: FeatureStatus.SUPPORTED,
|
||||
customCopyright: FeatureStatus.SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.ADVANCED,
|
||||
customAIPrompt: FeatureStatus.SUPPORTED,
|
||||
ssoLogin: 2000,
|
||||
visitorPermissionControl: FeatureStatus.SUPPORTED,
|
||||
pageWatermark: FeatureStatus.SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.ADVANCED,
|
||||
feishuQARobot: FeatureStatus.ADVANCED,
|
||||
dingtalkQARobot: FeatureStatus.ADVANCED,
|
||||
wecomQARobot: FeatureStatus.ADVANCED,
|
||||
wecomServiceRobot: FeatureStatus.ADVANCED,
|
||||
discordQARobot: FeatureStatus.ADVANCED,
|
||||
docVersionHistory: FeatureStatus.SUPPORTED,
|
||||
apiCall: FeatureStatus.SUPPORTED,
|
||||
sourceCode: FeatureStatus.NOT_SUPPORTED,
|
||||
},
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionEnterprise]: {
|
||||
label: '企业版',
|
||||
features: {
|
||||
wikiCount: Infinity,
|
||||
docCountPerWiki: Infinity,
|
||||
adminCount: Infinity,
|
||||
adminPermissionControl: FeatureStatus.SUPPORTED,
|
||||
seoConfig: FeatureStatus.ADVANCED,
|
||||
multiLanguage: FeatureStatus.SUPPORTED,
|
||||
customCopyright: FeatureStatus.SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.ADVANCED,
|
||||
customAIPrompt: FeatureStatus.SUPPORTED,
|
||||
ssoLogin: Infinity,
|
||||
visitorPermissionControl: FeatureStatus.SUPPORTED,
|
||||
pageWatermark: FeatureStatus.SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.ADVANCED,
|
||||
feishuQARobot: FeatureStatus.ADVANCED,
|
||||
dingtalkQARobot: FeatureStatus.ADVANCED,
|
||||
wecomQARobot: FeatureStatus.ADVANCED,
|
||||
wecomServiceRobot: FeatureStatus.ADVANCED,
|
||||
discordQARobot: FeatureStatus.ADVANCED,
|
||||
docVersionHistory: FeatureStatus.SUPPORTED,
|
||||
apiCall: FeatureStatus.SUPPORTED,
|
||||
sourceCode: FeatureStatus.SUPPORTED,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 功能特性标签映射
|
||||
*/
|
||||
export const FEATURE_LABELS: Record<string, string> = {
|
||||
wikiCount: 'Wiki 站点数量',
|
||||
docCountPerWiki: '每个 Wiki 的文档数量',
|
||||
adminCount: '管理员数量',
|
||||
adminPermissionControl: '管理员分权控制',
|
||||
seoConfig: 'SEO 配置',
|
||||
multiLanguage: '多语言支持',
|
||||
customCopyright: '自定义版权信息',
|
||||
trafficAnalysis: '访问流量分析',
|
||||
customAIPrompt: '自定义 AI 提示词',
|
||||
ssoLogin: 'SSO 登录',
|
||||
visitorPermissionControl: '访客权限控制',
|
||||
pageWatermark: '页面水印',
|
||||
contentNoCopy: '内容不可复制',
|
||||
sensitiveContentFilter: '敏感内容过滤',
|
||||
webWidgetRobot: '网页挂件机器人',
|
||||
feishuQARobot: '飞书问答机器人',
|
||||
dingtalkQARobot: '钉钉问答机器人',
|
||||
wecomQARobot: '企业微信问答机器人',
|
||||
wecomServiceRobot: '企业微信客服机器人',
|
||||
discordQARobot: 'Discord 问答机器人',
|
||||
docVersionHistory: '文档历史版本管理',
|
||||
apiCall: 'API 调用',
|
||||
sourceCode: '项目源码',
|
||||
};
|
||||
|
||||
/**
|
||||
* 功能状态显示文本映射
|
||||
*/
|
||||
export const FEATURE_STATUS_LABELS: Record<FeatureStatus, string> = {
|
||||
[FeatureStatus.NOT_SUPPORTED]: '不支持',
|
||||
[FeatureStatus.SUPPORTED]: '支持',
|
||||
[FeatureStatus.BASIC]: '基础配置',
|
||||
[FeatureStatus.ADVANCED]: '高级配置',
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取功能特性值
|
||||
*/
|
||||
export function getFeatureValue<K extends keyof VersionInfo['features']>(
|
||||
edition: ConstsLicenseEdition,
|
||||
key: K,
|
||||
): VersionInfo['features'][K] {
|
||||
return (
|
||||
VERSION_INFO[edition] ||
|
||||
VERSION_INFO[ConstsLicenseEdition.LicenseEditionFree]
|
||||
).features[key];
|
||||
}
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
export { useBindCaptcha } from './useBindCaptcha';
|
||||
export { useCommitPendingInput } from './useCommitPendingInput';
|
||||
export { useURLSearchParams } from './useURLSearchParams';
|
||||
export {
|
||||
useFeatureValue,
|
||||
useFeatureValueSupported,
|
||||
useVersionInfo,
|
||||
} from './useVersionFeature';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
FeatureStatus,
|
||||
VersionInfoMap,
|
||||
VersionInfo,
|
||||
getFeatureValue,
|
||||
} from '@/constant/version';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
export const useFeatureValue = <K extends keyof VersionInfo['features']>(
|
||||
key: K,
|
||||
): VersionInfo['features'][K] => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
return getFeatureValue(license.edition!, key);
|
||||
};
|
||||
|
||||
export const useFeatureValueSupported = (
|
||||
key: keyof VersionInfo['features'],
|
||||
) => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
return (
|
||||
getFeatureValue(license.edition!, key) === FeatureStatus.SUPPORTED ||
|
||||
getFeatureValue(license.edition!, key) === FeatureStatus.ADVANCED
|
||||
);
|
||||
};
|
||||
|
||||
export const useVersionInfo = () => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
return (
|
||||
VersionInfoMap[
|
||||
license.edition ?? ConstsLicenseEdition.LicenseEditionFree
|
||||
] || VersionInfoMap[ConstsLicenseEdition.LicenseEditionFree]
|
||||
);
|
||||
};
|
||||
|
|
@ -8,6 +8,8 @@ import { styled } from '@mui/material/styles';
|
|||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
import DocModal from './DocModal';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
import { useURLSearchParams } from '@/hooks';
|
||||
import {
|
||||
|
|
@ -46,7 +48,7 @@ const statusColorMap = {
|
|||
} as const;
|
||||
|
||||
export default function ContributionPage() {
|
||||
const { kb_id = '', kbDetail } = useAppSelector(state => state.config);
|
||||
const { kb_id = '', license } = useAppSelector(state => state.config);
|
||||
const [searchParams, setSearchParams] = useURLSearchParams();
|
||||
const page = Number(searchParams.get('page') || '1');
|
||||
const pageSize = Number(searchParams.get('page_size') || '20');
|
||||
|
|
@ -283,111 +285,114 @@ export default function ContributionPage() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (kb_id) getData();
|
||||
if (kb_id && PROFESSION_VERSION_PERMISSION.includes(license.edition!))
|
||||
getData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, pageSize, nodeNameParam, authNameParam, kb_id]);
|
||||
}, [page, pageSize, nodeNameParam, authNameParam, kb_id, license.edition]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
sx={{ p: 2 }}
|
||||
>
|
||||
<StyledSearchRow direction='row' sx={{ p: 0, flex: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='文档'
|
||||
value={searchDoc}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ node_name: searchDoc || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ node_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchDoc(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='用户'
|
||||
value={searchUser}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ auth_name: searchUser || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ auth_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchUser(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
</StyledSearchRow>
|
||||
</Stack>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey='id'
|
||||
height='calc(100vh - 148px)'
|
||||
size='small'
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
...tableSx,
|
||||
'.MuiTableContainer-root': {
|
||||
height: 'calc(100vh - 148px - 70px)',
|
||||
},
|
||||
}}
|
||||
pagination={{
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
onChange: (page, pageSize) => {
|
||||
setSearchParams({
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
});
|
||||
},
|
||||
}}
|
||||
PaginationProps={{
|
||||
sx: {
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
p: 2,
|
||||
'.MuiSelect-root': {
|
||||
width: 100,
|
||||
<VersionMask permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
sx={{ p: 2 }}
|
||||
>
|
||||
<StyledSearchRow direction='row' sx={{ p: 0, flex: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='文档'
|
||||
value={searchDoc}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ node_name: searchDoc || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ node_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchDoc(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='用户'
|
||||
value={searchUser}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ auth_name: searchUser || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ auth_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchUser(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
</StyledSearchRow>
|
||||
</Stack>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey='id'
|
||||
height='calc(100vh - 148px)'
|
||||
size='small'
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
...tableSx,
|
||||
'.MuiTableContainer-root': {
|
||||
height: 'calc(100vh - 148px - 70px)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
pagination={{
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
onChange: (page, pageSize) => {
|
||||
setSearchParams({
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
});
|
||||
},
|
||||
}}
|
||||
PaginationProps={{
|
||||
sx: {
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
p: 2,
|
||||
'.MuiSelect-root': {
|
||||
width: 100,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{previewRow?.meta?.content_type === 'md' ? (
|
||||
<MarkdownPreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
{previewRow?.meta?.content_type === 'md' ? (
|
||||
<MarkdownPreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
) : (
|
||||
<ContributePreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
)}
|
||||
<DocModal
|
||||
open={docModalOpen}
|
||||
onClose={() => setDocModalOpen(false)}
|
||||
onOk={handleDocModalOk}
|
||||
/>
|
||||
) : (
|
||||
<ContributePreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
)}
|
||||
<DocModal
|
||||
open={docModalOpen}
|
||||
onClose={() => setDocModalOpen(false)}
|
||||
onOk={handleDocModalOk}
|
||||
/>
|
||||
</VersionMask>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { ChatConversationPair } from '@/api';
|
|||
import { getApiV1ConversationDetail } from '@/request/Conversation';
|
||||
import { DomainConversationDetailResp } from '@/request/types';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import Card from '@/components/Card';
|
||||
import MarkDown from '@/components/MarkDown';
|
||||
import { useAppSelector } from '@/store';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
|
|
@ -13,10 +13,169 @@ import {
|
|||
Box,
|
||||
Stack,
|
||||
useTheme,
|
||||
styled,
|
||||
alpha,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Ellipsis, Icon, Modal } from '@ctzhian/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const handleThinkingContent = (content: string) => {
|
||||
const thinkRegex = /<think>([\s\S]*?)(?:<\/think>|$)/g;
|
||||
const thinkMatches = [];
|
||||
let match;
|
||||
while ((match = thinkRegex.exec(content)) !== null) {
|
||||
thinkMatches.push(match[1]);
|
||||
}
|
||||
|
||||
let answerContent = content.replace(/<think>[\s\S]*?<\/think>/g, '');
|
||||
answerContent = answerContent.replace(/<think>[\s\S]*$/, '');
|
||||
|
||||
return {
|
||||
thinkingContent: thinkMatches.join(''),
|
||||
answerContent: answerContent,
|
||||
};
|
||||
};
|
||||
|
||||
export const StyledConversationItem = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
}));
|
||||
|
||||
// 聊天气泡相关组件
|
||||
export const StyledUserBubble = styled(Box)(({ theme }) => ({
|
||||
alignSelf: 'flex-end',
|
||||
maxWidth: '75%',
|
||||
padding: theme.spacing(1, 2),
|
||||
borderRadius: '10px 10px 0px 10px',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
fontSize: 14,
|
||||
wordBreak: 'break-word',
|
||||
}));
|
||||
|
||||
export const StyledAiBubble = styled(Box)(({ theme }) => ({
|
||||
alignSelf: 'flex-start',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: theme.spacing(3),
|
||||
}));
|
||||
|
||||
export const StyledAiBubbleContent = styled(Box)(() => ({
|
||||
wordBreak: 'break-word',
|
||||
}));
|
||||
|
||||
// 对话相关组件
|
||||
export const StyledAccordion = styled(Accordion)(() => ({
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
'&:before': {
|
||||
content: '""',
|
||||
height: 0,
|
||||
},
|
||||
background: 'transparent',
|
||||
backgroundImage: 'none',
|
||||
}));
|
||||
|
||||
export const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
userSelect: 'text',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: theme.palette.background.paper3,
|
||||
border: '1px solid',
|
||||
borderColor: theme.palette.divider,
|
||||
}));
|
||||
|
||||
export const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
borderTop: 'none',
|
||||
}));
|
||||
|
||||
export const StyledQuestionText = styled(Box)(() => ({
|
||||
fontWeight: '700',
|
||||
fontSize: 16,
|
||||
lineHeight: '24px',
|
||||
wordBreak: 'break-all',
|
||||
}));
|
||||
|
||||
// 搜索结果相关组件
|
||||
export const StyledChunkAccordion = styled(Accordion)(({ theme }) => ({
|
||||
backgroundImage: 'none',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
}));
|
||||
|
||||
export const StyledChunkAccordionSummary = styled(AccordionSummary)(
|
||||
({ theme }) => ({
|
||||
justifyContent: 'flex-start',
|
||||
gap: theme.spacing(2),
|
||||
'.MuiAccordionSummary-content': {
|
||||
flexGrow: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const StyledChunkAccordionDetails = styled(AccordionDetails)(
|
||||
({ theme }) => ({
|
||||
paddingTop: 0,
|
||||
paddingLeft: theme.spacing(2),
|
||||
borderTop: 'none',
|
||||
borderLeft: '1px solid',
|
||||
borderColor: theme.palette.divider,
|
||||
}),
|
||||
);
|
||||
|
||||
export const StyledChunkItem = styled(Box)(({ theme }) => ({
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
'.hover-primary': {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// 思考过程相关组件
|
||||
export const StyledThinkingAccordion = styled(Accordion)(({ theme }) => ({
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
paddingBottom: theme.spacing(2),
|
||||
'&:before': {
|
||||
content: '""',
|
||||
height: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
export const StyledThinkingAccordionSummary = styled(AccordionSummary)(
|
||||
({ theme }) => ({
|
||||
justifyContent: 'flex-start',
|
||||
gap: theme.spacing(2),
|
||||
'.MuiAccordionSummary-content': {
|
||||
flexGrow: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const StyledThinkingAccordionDetails = styled(AccordionDetails)(
|
||||
({ theme }) => ({
|
||||
paddingTop: 0,
|
||||
paddingLeft: theme.spacing(2),
|
||||
borderTop: 'none',
|
||||
borderLeft: '1px solid',
|
||||
borderColor: theme.palette.divider,
|
||||
'.markdown-body': {
|
||||
opacity: 0.75,
|
||||
fontSize: 12,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const Detail = ({
|
||||
id,
|
||||
open,
|
||||
|
|
@ -55,7 +214,11 @@ const Detail = ({
|
|||
};
|
||||
} else if (message.role === 'assistant') {
|
||||
if (currentPair.user) {
|
||||
currentPair.assistant = message.content;
|
||||
const { thinkingContent, answerContent } = handleThinkingContent(
|
||||
message.content || '',
|
||||
);
|
||||
currentPair.assistant = answerContent;
|
||||
currentPair.thinking_content = thinkingContent;
|
||||
currentPair.created_at = message.created_at;
|
||||
// @ts-expect-error 类型不兼容
|
||||
currentPair.info = message.info;
|
||||
|
|
@ -167,26 +330,43 @@ const Detail = ({
|
|||
<Stack gap={2}>
|
||||
{conversations &&
|
||||
conversations.map((item, index) => (
|
||||
<Box key={index}>
|
||||
<Accordion defaultExpanded={true}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 24 }} />}
|
||||
sx={{
|
||||
userSelect: 'text',
|
||||
backgroundColor: 'background.paper3',
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{item.user}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<MarkDown
|
||||
content={item.assistant || '未查询到回答内容'}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Box>
|
||||
<StyledConversationItem key={index}>
|
||||
{/* 用户问题气泡 - 右对齐 */}
|
||||
<StyledUserBubble>{item.user}</StyledUserBubble>
|
||||
|
||||
{/* AI回答气泡 - 左对齐 */}
|
||||
<StyledAiBubble>
|
||||
{/* 思考过程 */}
|
||||
{!!item.thinking_content && (
|
||||
<StyledThinkingAccordion defaultExpanded>
|
||||
<StyledThinkingAccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
<Typography
|
||||
variant='body2'
|
||||
sx={theme => ({
|
||||
fontSize: 12,
|
||||
color: alpha(theme.palette.text.primary, 0.5),
|
||||
})}
|
||||
>
|
||||
已思考
|
||||
</Typography>
|
||||
</Stack>
|
||||
</StyledThinkingAccordionSummary>
|
||||
|
||||
<StyledThinkingAccordionDetails>
|
||||
<MarkDown content={item.thinking_content || ''} />
|
||||
</StyledThinkingAccordionDetails>
|
||||
</StyledThinkingAccordion>
|
||||
)}
|
||||
|
||||
{/* AI回答内容 */}
|
||||
<StyledAiBubbleContent>
|
||||
<MarkDown content={item.assistant} />
|
||||
</StyledAiBubbleContent>
|
||||
</StyledAiBubble>
|
||||
</StyledConversationItem>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import {
|
|||
import dayjs from 'dayjs';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
|
||||
interface DocPropertiesModalProps {
|
||||
open: boolean;
|
||||
|
|
@ -40,8 +42,6 @@ interface DocPropertiesModalProps {
|
|||
data: DomainNodeListItemResp[];
|
||||
}
|
||||
|
||||
const tips = '(企业版可用)';
|
||||
|
||||
const StyledText = styled('div')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 16,
|
||||
|
|
@ -53,7 +53,12 @@ const PER_OPTIONS = [
|
|||
value: ConstsNodeAccessPerm.NodeAccessPermOpen,
|
||||
},
|
||||
{
|
||||
label: '部分开放',
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<span>部分开放</span>
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: ConstsNodeAccessPerm.NodeAccessPermPartial,
|
||||
},
|
||||
{
|
||||
|
|
@ -128,13 +133,13 @@ const DocPropertiesModal = ({
|
|||
visitable: values.visitable as ConstsNodeAccessPerm,
|
||||
visible: values.visible as ConstsNodeAccessPerm,
|
||||
},
|
||||
answerable_groups: isEnterprise
|
||||
answerable_groups: isBusiness
|
||||
? values.answerable_groups.map(item => item.id!)
|
||||
: undefined,
|
||||
visitable_groups: isEnterprise
|
||||
visitable_groups: isBusiness
|
||||
? values.visitable_groups.map(item => item.id!)
|
||||
: undefined,
|
||||
visible_groups: isEnterprise
|
||||
visible_groups: isBusiness
|
||||
? values.visible_groups.map(item => item.id!)
|
||||
: undefined,
|
||||
}),
|
||||
|
|
@ -153,15 +158,15 @@ const DocPropertiesModal = ({
|
|||
});
|
||||
});
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const tree = filterEmptyFolders(convertToTree(data));
|
||||
|
||||
useEffect(() => {
|
||||
if (open && data) {
|
||||
if (isEnterprise) {
|
||||
if (isBusiness) {
|
||||
getApiProV1AuthGroupList({
|
||||
kb_id: kb_id!,
|
||||
page: 1,
|
||||
|
|
@ -206,7 +211,7 @@ const DocPropertiesModal = ({
|
|||
);
|
||||
});
|
||||
}
|
||||
}, [open, data, isEnterprise]);
|
||||
}, [open, data, isBusiness]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
|
|
@ -302,22 +307,15 @@ const DocPropertiesModal = ({
|
|||
name='answerable'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup row {...field}>
|
||||
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||
{PER_OPTIONS.map(option => (
|
||||
<FormControlLabel
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
option.label +
|
||||
(!isEnterprise &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
? tips
|
||||
: '')
|
||||
}
|
||||
label={option.label}
|
||||
disabled={
|
||||
!isEnterprise &&
|
||||
!isBusiness &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
}
|
||||
|
|
@ -359,22 +357,15 @@ const DocPropertiesModal = ({
|
|||
name='visitable'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup row {...field}>
|
||||
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||
{PER_OPTIONS.map(option => (
|
||||
<FormControlLabel
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
option.label +
|
||||
(!isEnterprise &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
? tips
|
||||
: '')
|
||||
}
|
||||
label={option.label}
|
||||
disabled={
|
||||
!isEnterprise &&
|
||||
!isBusiness &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
}
|
||||
|
|
@ -416,22 +407,15 @@ const DocPropertiesModal = ({
|
|||
name='visible'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup row {...field}>
|
||||
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||
{PER_OPTIONS.map(option => (
|
||||
<FormControlLabel
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
option.label +
|
||||
(!isEnterprise &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
? tips
|
||||
: '')
|
||||
}
|
||||
label={option.label}
|
||||
disabled={
|
||||
!isEnterprise &&
|
||||
!isBusiness &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import { useNavigate, useOutletContext } from 'react-router-dom';
|
|||
import { WrapContext } from '..';
|
||||
import DocAddByCustomText from '../../component/DocAddByCustomText';
|
||||
import DocDelete from '../../component/DocDelete';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
|
||||
interface HeaderProps {
|
||||
edit: boolean;
|
||||
|
|
@ -52,8 +54,8 @@ const Header = ({
|
|||
|
||||
const [showSaveTip, setShowSaveTip] = useState(false);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const handlePublish = useCallback(() => {
|
||||
|
|
@ -309,6 +311,7 @@ const Header = ({
|
|||
// },
|
||||
{
|
||||
key: 'copy',
|
||||
textSx: { flex: 1 },
|
||||
label: <StyledMenuSelect>复制</StyledMenuSelect>,
|
||||
onClick: () => {
|
||||
if (kb_id) {
|
||||
|
|
@ -328,26 +331,22 @@ const Header = ({
|
|||
},
|
||||
{
|
||||
key: 'version',
|
||||
textSx: { flex: 1 },
|
||||
label: (
|
||||
<StyledMenuSelect disabled={!isEnterprise}>
|
||||
历史版本{' '}
|
||||
{!isEnterprise && (
|
||||
<Tooltip title='企业版可用' placement='top' arrow>
|
||||
<InfoIcon
|
||||
sx={{ color: 'text.secondary', fontSize: 14 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<StyledMenuSelect disabled={!isBusiness}>
|
||||
历史版本
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</StyledMenuSelect>
|
||||
),
|
||||
onClick: () => {
|
||||
if (isEnterprise) {
|
||||
if (isBusiness) {
|
||||
navigate(`/doc/editor/history/${detail.id}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'rename',
|
||||
textSx: { flex: 1 },
|
||||
label: <StyledMenuSelect>重命名</StyledMenuSelect>,
|
||||
onClick: () => {
|
||||
setRenameOpen(true);
|
||||
|
|
@ -355,6 +354,7 @@ const Header = ({
|
|||
},
|
||||
{
|
||||
key: 'delete',
|
||||
textSx: { flex: 1 },
|
||||
label: <StyledMenuSelect>删除</StyledMenuSelect>,
|
||||
onClick: () => {
|
||||
setDelOpen(true);
|
||||
|
|
@ -566,7 +566,7 @@ const StyledMenuSelect = styled('div')<{ disabled?: boolean }>(
|
|||
padding: theme.spacing(0, 2),
|
||||
lineHeight: '40px',
|
||||
height: 40,
|
||||
width: 106,
|
||||
minWidth: 106,
|
||||
borderRadius: '5px',
|
||||
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import Header from './Header';
|
|||
import Summary from './Summary';
|
||||
import Toc from './Toc';
|
||||
import Toolbar from './Toolbar';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface WrapProps {
|
||||
detail: V1NodeDetailResp;
|
||||
|
|
@ -72,8 +73,8 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
|||
emoji: defaultDetail.meta?.emoji || '',
|
||||
});
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const debouncedUpdateSummary = useCallback(
|
||||
|
|
@ -383,7 +384,7 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
|||
</Stack>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip arrow title={isEnterprise ? '查看历史版本' : ''}>
|
||||
<Tooltip arrow title={isBusiness ? '查看历史版本' : ''}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
|
|
@ -391,13 +392,13 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
|||
sx={{
|
||||
fontSize: 12,
|
||||
color: 'text.tertiary',
|
||||
cursor: isEnterprise ? 'pointer' : 'text',
|
||||
cursor: isBusiness ? 'pointer' : 'text',
|
||||
':hover': {
|
||||
color: isEnterprise ? 'primary.main' : 'text.tertiary',
|
||||
color: isBusiness ? 'primary.main' : 'text.tertiary',
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isEnterprise) {
|
||||
if (isBusiness) {
|
||||
navigate(`/doc/editor/history/${defaultDetail.id}`);
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
IconButton,
|
||||
Stack,
|
||||
useTheme,
|
||||
ButtonBase,
|
||||
} from '@mui/material';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import VersionPublish from '../release/components/VersionPublish';
|
||||
|
|
@ -418,15 +419,19 @@ const Content = () => {
|
|||
>
|
||||
{publish.unpublished} 个 文档/文件夹未发布,
|
||||
</Box>
|
||||
<Button
|
||||
size='small'
|
||||
sx={{ minWidth: 0, p: 0, fontSize: 12 }}
|
||||
<ButtonBase
|
||||
disableRipple
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
color: 'primary.main',
|
||||
}}
|
||||
onClick={() => {
|
||||
setPublishOpen(true);
|
||||
}}
|
||||
>
|
||||
去发布
|
||||
</Button>
|
||||
</ButtonBase>
|
||||
</>
|
||||
)}
|
||||
{ragReStartCount > 0 && (
|
||||
|
|
@ -441,15 +446,19 @@ const Content = () => {
|
|||
>
|
||||
{ragReStartCount} 个文档未学习,
|
||||
</Box>
|
||||
<Button
|
||||
size='small'
|
||||
sx={{ minWidth: 0, p: 0, fontSize: 12 }}
|
||||
<ButtonBase
|
||||
disableRipple
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
color: 'primary.main',
|
||||
}}
|
||||
onClick={() => {
|
||||
setRagOpen(true);
|
||||
}}
|
||||
>
|
||||
去学习
|
||||
</Button>
|
||||
</ButtonBase>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
ButtonBase,
|
||||
} from '@mui/material';
|
||||
import { Ellipsis, Table, Modal, Icon, message } from '@ctzhian/ui';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
|
||||
|
|
@ -162,8 +163,8 @@ const Comments = ({
|
|||
useState<DomainWebAppCommentSettings | null>(null);
|
||||
|
||||
const isEnableReview = useMemo(() => {
|
||||
return !!(license.edition === 1 || license.edition === 2);
|
||||
}, [license]);
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCommentsFilter(isEnableReview);
|
||||
|
|
|
|||
|
|
@ -3,14 +3,18 @@ import { getApiV1ConversationMessageDetail } from '@/request';
|
|||
import MarkDown from '@/components/MarkDown';
|
||||
import { useAppSelector } from '@/store';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import { Ellipsis, Icon, Modal } from '@ctzhian/ui';
|
||||
import { Box, Stack, Typography, alpha } from '@mui/material';
|
||||
import { Ellipsis, Modal } from '@ctzhian/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
StyledConversationItem,
|
||||
StyledUserBubble,
|
||||
StyledAiBubble,
|
||||
StyledThinkingAccordion,
|
||||
StyledThinkingAccordionSummary,
|
||||
StyledThinkingAccordionDetails,
|
||||
StyledAiBubbleContent,
|
||||
} from '../conversation/Detail';
|
||||
|
||||
const Detail = ({
|
||||
id,
|
||||
|
|
@ -36,6 +40,7 @@ const Detail = ({
|
|||
user: data.question,
|
||||
assistant: res.content!,
|
||||
created_at: res.created_at!,
|
||||
thinking_content: '',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -62,24 +67,43 @@ const Detail = ({
|
|||
>
|
||||
<Box sx={{ fontSize: 14 }}>
|
||||
<Box>
|
||||
<Accordion defaultExpanded={true}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 24 }} />}
|
||||
sx={{
|
||||
userSelect: 'text',
|
||||
backgroundColor: 'background.paper3',
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{conversations?.user}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<MarkDown
|
||||
content={conversations?.assistant || '未查询到回答内容'}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<StyledConversationItem>
|
||||
{/* 用户问题气泡 - 右对齐 */}
|
||||
<StyledUserBubble>{conversations?.user}</StyledUserBubble>
|
||||
|
||||
{/* AI回答气泡 - 左对齐 */}
|
||||
<StyledAiBubble>
|
||||
{/* 思考过程 */}
|
||||
{!!conversations?.thinking_content && (
|
||||
<StyledThinkingAccordion defaultExpanded>
|
||||
<StyledThinkingAccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
<Typography
|
||||
variant='body2'
|
||||
sx={theme => ({
|
||||
fontSize: 12,
|
||||
color: alpha(theme.palette.text.primary, 0.5),
|
||||
})}
|
||||
>
|
||||
已思考
|
||||
</Typography>
|
||||
</Stack>
|
||||
</StyledThinkingAccordionSummary>
|
||||
|
||||
<StyledThinkingAccordionDetails>
|
||||
<MarkDown content={conversations?.thinking_content || ''} />
|
||||
</StyledThinkingAccordionDetails>
|
||||
</StyledThinkingAccordion>
|
||||
)}
|
||||
|
||||
{/* AI回答内容 */}
|
||||
<StyledAiBubbleContent>
|
||||
<MarkDown content={conversations?.assistant || ''} />
|
||||
</StyledAiBubbleContent>
|
||||
</StyledAiBubble>
|
||||
</StyledConversationItem>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import dayjs from 'dayjs';
|
|||
import { ColumnType } from '@ctzhian/ui/dist/Table';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface AddRoleProps {
|
||||
open: boolean;
|
||||
|
|
@ -23,7 +25,8 @@ interface AddRoleProps {
|
|||
}
|
||||
|
||||
const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
||||
const { kb_id, license } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
const [list, setList] = useState<V1UserListItemResp[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string>('');
|
||||
|
|
@ -31,10 +34,6 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
|||
ConstsUserKBPermission.UserKBPermissionFullControl,
|
||||
);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const columns: ColumnType<V1UserListItemResp>[] = [
|
||||
{
|
||||
title: '',
|
||||
|
|
@ -119,6 +118,10 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
|||
}
|
||||
}, [open]);
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title='添加 Wiki 站管理员'
|
||||
|
|
@ -209,22 +212,33 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
|||
fullWidth
|
||||
sx={{ height: 52 }}
|
||||
value={perm}
|
||||
MenuProps={{
|
||||
sx: {
|
||||
'.Mui-disabled': {
|
||||
opacity: '1 !important',
|
||||
color: 'text.disabled',
|
||||
},
|
||||
},
|
||||
}}
|
||||
onChange={e => setPerm(e.target.value as V1KBUserInviteReq['perm'])}
|
||||
>
|
||||
<MenuItem value={ConstsUserKBPermission.UserKBPermissionFullControl}>
|
||||
完全控制
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||
disabled={!isPro}
|
||||
>
|
||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
||||
文档管理{' '}
|
||||
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||
disabled={!isPro}
|
||||
>
|
||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
||||
数据运营{' '}
|
||||
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { getApiProV1Prompt, postApiProV1Prompt } from '@/request/pro/Prompt';
|
||||
import { DomainKnowledgeBaseDetail } from '@/request/types';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { Box, Slider, TextField } from '@mui/material';
|
||||
|
|
@ -33,11 +34,12 @@ const CardAI = ({ kb }: CardAIProps) => {
|
|||
});
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return license.edition === 1 || license.edition === 2;
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb.id || !isPro) return;
|
||||
if (!kb.id || !PROFESSION_VERSION_PERMISSION.includes(license.edition!))
|
||||
return;
|
||||
getApiProV1Prompt({ kb_id: kb.id! }).then(res => {
|
||||
setValue('content', res.content || '');
|
||||
});
|
||||
|
|
@ -54,7 +56,7 @@ const CardAI = ({ kb }: CardAIProps) => {
|
|||
<SettingCardItem title='智能问答' isEdit={isEdit} onSubmit={onSubmit}>
|
||||
<FormItem
|
||||
vertical
|
||||
tooltip={!isPro && '联创版和企业版可用'}
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
extra={
|
||||
<Box
|
||||
sx={{
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import { ColumnType } from '@ctzhian/ui/dist/Table';
|
|||
import { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import { SettingCardItem, FormItem } from './Common';
|
||||
|
||||
interface CardAuthProps {
|
||||
|
|
@ -114,7 +116,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
}),
|
||||
value.enabled === '2' &&
|
||||
source_type !== EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword
|
||||
? isPro
|
||||
? isBusiness
|
||||
? postApiProV1AuthSet({
|
||||
kb_id,
|
||||
source_type: value.source_type as ConstsSourceType,
|
||||
|
|
@ -157,25 +159,18 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
});
|
||||
});
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return license.edition === 1 || license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
useEffect(() => {
|
||||
const source_type = isPro
|
||||
const source_type = isBusiness
|
||||
? kb.access_settings?.source_type ||
|
||||
EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword
|
||||
: kb.access_settings?.source_type ===
|
||||
EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub
|
||||
? EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub
|
||||
: EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword;
|
||||
: EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword;
|
||||
setValue('source_type', source_type);
|
||||
sourceTypeRef.current = source_type;
|
||||
}, [kb, isPro]);
|
||||
}, [kb, isBusiness]);
|
||||
|
||||
useEffect(() => {
|
||||
if (kb.access_settings?.simple_auth) {
|
||||
|
|
@ -191,7 +186,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
}, [kb]);
|
||||
|
||||
const getAuth = () => {
|
||||
if (isPro) {
|
||||
if (isBusiness) {
|
||||
getApiProV1AuthGet({
|
||||
kb_id,
|
||||
source_type: source_type as ConstsSourceType,
|
||||
|
|
@ -236,7 +231,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
useEffect(() => {
|
||||
if (!kb_id || enabled !== '2') return;
|
||||
getAuth();
|
||||
}, [kb_id, isPro, source_type, enabled]);
|
||||
}, [kb_id, isBusiness, source_type, enabled]);
|
||||
|
||||
const columns: ColumnType<GithubComChaitinPandaWikiProApiAuthV1AuthItem>[] = [
|
||||
{
|
||||
|
|
@ -875,8 +870,18 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
MenuProps={{
|
||||
sx: {
|
||||
'.Mui-disabled': {
|
||||
opacity: '1 !important',
|
||||
color: 'text.disabled',
|
||||
},
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
sx={{ height: 52 }}
|
||||
sx={{
|
||||
height: 52,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword}
|
||||
|
|
@ -885,44 +890,52 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeDingTalk}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
钉钉登录 {isPro ? '' : tips}
|
||||
钉钉登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeFeishu}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
飞书登录 {isPro ? '' : tips}
|
||||
飞书登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeWeCom}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
企业微信登录 {isPro ? '' : tips}
|
||||
企业微信登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeOAuth}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
OAuth 登录 {isPro ? '' : tips}
|
||||
OAuth 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeCAS}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
CAS 登录 {isPro ? '' : tips}
|
||||
CAS 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeLDAP}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
LDAP 登录 {isPro ? '' : tips}
|
||||
LDAP 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
GitHub 登录
|
||||
GitHub 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
DomainKnowledgeBaseDetail,
|
||||
} from '@/request/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
|
|
@ -12,7 +12,6 @@ import {
|
|||
RadioGroup,
|
||||
styled,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
|
||||
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
||||
|
|
@ -37,7 +36,7 @@ const DocumentComments = ({
|
|||
data: DomainAppDetailResp;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
|
|
@ -57,8 +56,6 @@ const DocumentComments = ({
|
|||
);
|
||||
}, [data]);
|
||||
|
||||
const isPro = license.edition === 1 || license.edition === 2;
|
||||
|
||||
const onSubmit = handleSubmit(formData => {
|
||||
putApiV1App(
|
||||
{ id: data.id! },
|
||||
|
|
@ -108,7 +105,7 @@ const DocumentComments = ({
|
|||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='评论审核' tooltip={!isPro && '联创版和企业版可用'}>
|
||||
<FormItem label='评论审核' permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='moderation_enable'
|
||||
|
|
@ -116,7 +113,6 @@ const DocumentComments = ({
|
|||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
value={isPro ? field.value : undefined}
|
||||
onChange={e => {
|
||||
setIsEdit(true);
|
||||
field.onChange(+e.target.value as 1 | 0);
|
||||
|
|
@ -124,12 +120,12 @@ const DocumentComments = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={1}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>启用</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={0}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
@ -150,7 +146,7 @@ const AIQuestion = ({
|
|||
refresh: () => void;
|
||||
}) => {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const { kb_id, license } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
is_enabled: true,
|
||||
|
|
@ -159,7 +155,6 @@ const AIQuestion = ({
|
|||
},
|
||||
});
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const isEnterprise = license.edition === 2;
|
||||
|
||||
const onSubmit = handleSubmit(formData => {
|
||||
putApiV1App(
|
||||
|
|
@ -273,7 +268,7 @@ const AIQuestion = ({
|
|||
)}
|
||||
/>{' '}
|
||||
</FormItem>
|
||||
<FormItem label='免责声明' tooltip={!isEnterprise && '企业版可用'}>
|
||||
<FormItem label='免责声明' permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='disclaimer'
|
||||
|
|
@ -282,7 +277,6 @@ const AIQuestion = ({
|
|||
{...field}
|
||||
fullWidth
|
||||
value={field.value || ''}
|
||||
disabled={!isEnterprise}
|
||||
placeholder='请输入免责声明'
|
||||
onChange={e => {
|
||||
setIsEdit(true);
|
||||
|
|
@ -304,7 +298,7 @@ const DocumentContribution = ({
|
|||
refresh: () => void;
|
||||
}) => {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
is_enable: false,
|
||||
|
|
@ -330,7 +324,6 @@ const DocumentContribution = ({
|
|||
});
|
||||
});
|
||||
|
||||
const isPro = license.edition === 1 || license.edition === 2;
|
||||
useEffect(() => {
|
||||
setValue(
|
||||
'is_enable',
|
||||
|
|
@ -340,21 +333,8 @@ const DocumentContribution = ({
|
|||
}, [data]);
|
||||
|
||||
return (
|
||||
<SettingCardItem
|
||||
title={
|
||||
<>
|
||||
文档贡献
|
||||
{!isPro && (
|
||||
<Tooltip title='联创版和企业版可用' placement='top' arrow>
|
||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
isEdit={isEdit}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<FormItem label='文档贡献'>
|
||||
<SettingCardItem title='文档贡献' isEdit={isEdit} onSubmit={onSubmit}>
|
||||
<FormItem label='文档贡献' permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='is_enable'
|
||||
|
|
@ -362,7 +342,7 @@ const DocumentContribution = ({
|
|||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
value={isPro ? field.value : undefined}
|
||||
value={field.value}
|
||||
onChange={e => {
|
||||
setIsEdit(true);
|
||||
field.onChange(e.target.value === 'true');
|
||||
|
|
@ -370,12 +350,12 @@ const DocumentContribution = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={true}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>启用</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={false}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ import { Controller, useForm } from 'react-hook-form';
|
|||
import { useDispatch } from 'react-redux';
|
||||
import AddRole from './AddRole';
|
||||
import { Form, FormItem, SettingCardItem } from './Common';
|
||||
import {
|
||||
PROFESSION_VERSION_PERMISSION,
|
||||
BUSINESS_VERSION_PERMISSION,
|
||||
} from '@/constant/version';
|
||||
|
||||
type ApiTokenPermission =
|
||||
GithubComChaitinPandaWikiProApiTokenV1CreateAPITokenReq['permission'];
|
||||
|
|
@ -69,8 +73,8 @@ const ApiToken = () => {
|
|||
perm: ConstsUserKBPermission.UserKBPermissionFullControl,
|
||||
},
|
||||
});
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const onDeleteApiToken = (id: string, name: string) => {
|
||||
|
|
@ -131,9 +135,9 @@ const ApiToken = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb_id) return;
|
||||
if (!kb_id || !isBusiness) return;
|
||||
getApiTokenList();
|
||||
}, [kb_id]);
|
||||
}, [kb_id, isBusiness]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!addOpen) reset();
|
||||
|
|
@ -142,27 +146,17 @@ const ApiToken = () => {
|
|||
return (
|
||||
<SettingCardItem
|
||||
title='API Token'
|
||||
permission={BUSINESS_VERSION_PERMISSION}
|
||||
extra={
|
||||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<Button
|
||||
color='primary'
|
||||
size='small'
|
||||
disabled={!isEnterprise}
|
||||
onClick={() => setAddOpen(true)}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
创建 API Token
|
||||
</Button>
|
||||
|
||||
<Tooltip title={'企业版可用'} placement='top' arrow>
|
||||
<InfoIcon
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
fontSize: 14,
|
||||
display: !isEnterprise ? 'block' : 'none',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
|
|
@ -232,7 +226,7 @@ const ApiToken = () => {
|
|||
size='small'
|
||||
sx={{ width: 120 }}
|
||||
value={it.permission}
|
||||
disabled={!isEnterprise || user.role !== 'admin'}
|
||||
disabled={!isBusiness || user.role !== 'admin'}
|
||||
onChange={e =>
|
||||
onUpdateApiToken(it.id!, e.target.value as ApiTokenPermission)
|
||||
}
|
||||
|
|
@ -259,7 +253,7 @@ const ApiToken = () => {
|
|||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? '权限不足'
|
||||
: '企业版可用'
|
||||
: '商业版可用'
|
||||
}
|
||||
placement='top'
|
||||
arrow
|
||||
|
|
@ -270,7 +264,7 @@ const ApiToken = () => {
|
|||
fontSize: 14,
|
||||
ml: 1,
|
||||
visibility:
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? 'visible'
|
||||
|
|
@ -285,13 +279,13 @@ const ApiToken = () => {
|
|||
type='icon-icon_tool_close'
|
||||
sx={{
|
||||
cursor:
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? 'not-allowed'
|
||||
: 'pointer',
|
||||
color:
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? 'text.disabled'
|
||||
|
|
@ -299,7 +293,7 @@ const ApiToken = () => {
|
|||
}}
|
||||
onClick={() => {
|
||||
if (
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
)
|
||||
|
|
@ -367,17 +361,16 @@ const ApiToken = () => {
|
|||
>
|
||||
完全控制
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||
>
|
||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
||||
文档管理
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||
>
|
||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
||||
数据运营
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
|
|
@ -405,9 +398,9 @@ const CardKB = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
const isPro = useMemo(() => {
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb_id) return;
|
||||
|
|
@ -513,7 +506,7 @@ const CardKB = () => {
|
|||
size='small'
|
||||
sx={{ width: 180 }}
|
||||
value={it.perms}
|
||||
disabled={!isEnterprise || it.role === 'admin'}
|
||||
disabled={!isPro || it.role === 'admin'}
|
||||
onChange={e =>
|
||||
onUpdateUserPermission(
|
||||
it.id!,
|
||||
|
|
@ -542,7 +535,7 @@ const CardKB = () => {
|
|||
title={
|
||||
it.role === 'admin'
|
||||
? '超级管理员不可被修改权限'
|
||||
: '企业版可用'
|
||||
: '专业版可用'
|
||||
}
|
||||
placement='top'
|
||||
arrow
|
||||
|
|
@ -553,9 +546,7 @@ const CardKB = () => {
|
|||
fontSize: 14,
|
||||
ml: 1,
|
||||
visibility:
|
||||
!isEnterprise || it.role === 'admin'
|
||||
? 'visible'
|
||||
: 'hidden',
|
||||
!isPro || it.role === 'admin' ? 'visible' : 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ import {
|
|||
} from '@/request/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { Icon, message } from '@ctzhian/ui';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Collapse,
|
||||
FormControlLabel,
|
||||
Link,
|
||||
Radio,
|
||||
|
|
@ -31,6 +34,8 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
|
||||
const [widgetConfigOpen, setWidgetConfigOpen] = useState(false);
|
||||
const [modalConfigOpen, setModalConfigOpen] = useState(false);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const {
|
||||
control,
|
||||
|
|
@ -43,8 +48,15 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
defaultValues: {
|
||||
is_open: 0,
|
||||
theme_mode: 'light',
|
||||
btn_style: 'hover_ball',
|
||||
btn_id: '',
|
||||
btn_position: 'bottom_right',
|
||||
disclaimer: '',
|
||||
btn_text: '',
|
||||
btn_logo: '',
|
||||
modal_position: 'follow',
|
||||
search_mode: 'all',
|
||||
placeholder: '',
|
||||
recommend_questions: [] as string[],
|
||||
recommend_node_ids: [] as string[],
|
||||
},
|
||||
|
|
@ -54,6 +66,8 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
|
||||
const recommend_questions = watch('recommend_questions') || [];
|
||||
const recommend_node_ids = watch('recommend_node_ids') || [];
|
||||
const btn_style = watch('btn_style') || 'hover_ball';
|
||||
const isCustomButton = btn_style === 'btn_trigger';
|
||||
|
||||
const recommendQuestionsField = useCommitPendingInput<string>({
|
||||
value: recommend_questions,
|
||||
|
|
@ -87,8 +101,17 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
reset({
|
||||
is_open: res.settings?.widget_bot_settings?.is_open ? 1 : 0,
|
||||
theme_mode: res.settings?.widget_bot_settings?.theme_mode || 'light',
|
||||
btn_style: res.settings?.widget_bot_settings?.btn_style || 'hover_ball',
|
||||
btn_id: res.settings?.widget_bot_settings?.btn_id || '',
|
||||
btn_position:
|
||||
res.settings?.widget_bot_settings?.btn_position || 'bottom_right',
|
||||
btn_text: res.settings?.widget_bot_settings?.btn_text || '在线客服',
|
||||
btn_logo: res.settings?.widget_bot_settings?.btn_logo,
|
||||
btn_logo: res.settings?.widget_bot_settings?.btn_logo || '',
|
||||
modal_position:
|
||||
res.settings?.widget_bot_settings?.modal_position || 'follow',
|
||||
search_mode: res.settings?.widget_bot_settings?.search_mode || 'all',
|
||||
placeholder: res.settings?.widget_bot_settings?.placeholder || '',
|
||||
disclaimer: res.settings?.widget_bot_settings?.disclaimer || '',
|
||||
recommend_questions:
|
||||
res.settings?.widget_bot_settings?.recommend_questions || [],
|
||||
recommend_node_ids:
|
||||
|
|
@ -108,8 +131,15 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
widget_bot_settings: {
|
||||
is_open: data.is_open === 1 ? true : false,
|
||||
theme_mode: data.theme_mode as 'light' | 'dark',
|
||||
btn_style: data.btn_style,
|
||||
btn_id: data.btn_id,
|
||||
btn_position: data.btn_position,
|
||||
btn_text: data.btn_text,
|
||||
btn_logo: data.btn_logo,
|
||||
modal_position: data.modal_position,
|
||||
search_mode: data.search_mode,
|
||||
placeholder: data.placeholder,
|
||||
disclaimer: data.disclaimer,
|
||||
recommend_questions: data.recommend_questions || [],
|
||||
recommend_node_ids: data.recommend_node_ids || [],
|
||||
},
|
||||
|
|
@ -151,146 +181,469 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
</Link>
|
||||
}
|
||||
>
|
||||
<FormItem label='网页挂件机器人'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='is_open'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(+e.target.value as 1 | 0);
|
||||
setIsEnabled((+e.target.value as 1 | 0) === 1);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
<Stack spacing={3}>
|
||||
<FormItem label='网页挂件机器人'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='is_open'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(+e.target.value as 1 | 0);
|
||||
setIsEnabled((+e.target.value as 1 | 0) === 1);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={1}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>启用</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={0}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>禁用</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
{isEnabled && (
|
||||
<>
|
||||
<FormItem
|
||||
label='嵌入代码'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={1}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>启用</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={0}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>禁用</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
{isEnabled && (
|
||||
<>
|
||||
<FormItem label='配色方案'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='theme_mode'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
{url ? (
|
||||
<ShowText
|
||||
noEllipsis
|
||||
text={[
|
||||
`<!--// Head 标签引入样式 -->`,
|
||||
`<link rel="stylesheet" href="${url}/widget-bot.css">`,
|
||||
`<!--// Body 标签引入挂件 -->`,
|
||||
`<script src="${url}/widget-bot.js"></script>`,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems={'center'}
|
||||
gap={0.5}
|
||||
sx={{
|
||||
color: 'warning.main',
|
||||
fontSize: 14,
|
||||
p: 1.5,
|
||||
borderRadius: 1,
|
||||
bgcolor: 'warning.light',
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={'light'}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>浅色模式</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={'dark'}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>深色模式</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
<Icon type='icon-jinggao' />
|
||||
未配置域名,可在右侧
|
||||
<Box component={'span'} sx={{ fontWeight: 500 }}>
|
||||
服务监听方式
|
||||
</Box>{' '}
|
||||
中配置
|
||||
</Stack>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='侧边按钮文字'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_text'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
{...field}
|
||||
placeholder='输入侧边按钮文字'
|
||||
error={!!errors.btn_text}
|
||||
helperText={errors.btn_text?.message}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='侧边按钮 Logo'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_logo'
|
||||
render={({ field }) => (
|
||||
<UploadFile
|
||||
{...field}
|
||||
id='btn_logo'
|
||||
type='url'
|
||||
accept='image/*'
|
||||
width={80}
|
||||
onChange={url => {
|
||||
field.onChange(url);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='推荐问题'>
|
||||
<FreeSoloAutocomplete
|
||||
{...recommendQuestionsField}
|
||||
placeholder='回车确认,填写下一个推荐问题'
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='推荐文档'>
|
||||
<RecommendDocDragList
|
||||
ids={recommend_node_ids}
|
||||
onChange={(value: string[]) => {
|
||||
setIsEdit(true);
|
||||
setValue('recommend_node_ids', value);
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='嵌入代码'>
|
||||
{url ? (
|
||||
<ShowText
|
||||
noEllipsis
|
||||
text={[
|
||||
`<!--// Head 标签引入样式 -->`,
|
||||
`<link rel="stylesheet" href="${url}/widget-bot.css">`,
|
||||
'',
|
||||
`<!--// Body 标签引入挂件 -->`,
|
||||
`<script src="${url}/widget-bot.js"></script>`,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems={'center'}
|
||||
gap={0.5}
|
||||
sx={{ color: 'warning.main', fontSize: 14 }}
|
||||
>
|
||||
<Icon type='icon-jinggao' />
|
||||
未配置域名,可在右侧
|
||||
<Box component={'span'} sx={{ fontWeight: 500 }}>
|
||||
服务监听方式
|
||||
</Box>{' '}
|
||||
中配置
|
||||
</Stack>
|
||||
)}
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='挂件配置'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Box>
|
||||
{!widgetConfigOpen && (
|
||||
<Button
|
||||
size='small'
|
||||
variant='outlined'
|
||||
onClick={() => setWidgetConfigOpen(true)}
|
||||
endIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
展开
|
||||
</Button>
|
||||
)}
|
||||
<Collapse in={widgetConfigOpen}>
|
||||
<Stack spacing={2.5}>
|
||||
<FormItem
|
||||
label='按钮样式'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_style'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
const value = e.target.value;
|
||||
field.onChange(value);
|
||||
if (value === 'btn_trigger') {
|
||||
setValue('modal_position', 'fixed');
|
||||
}
|
||||
setIsEdit(true);
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value='hover_ball'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>悬浮球</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='side_sticky'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>侧边吸附</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='btn_trigger'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>自定义按钮</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
{isCustomButton ? (
|
||||
<FormItem
|
||||
label='自定义按钮 ID'
|
||||
required
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_id'
|
||||
rules={{
|
||||
required: '自定义按钮 ID 不能为空',
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
placeholder='嵌入网站中自定义按钮的 #id 点击触发,如: pandawiki-widget-bot-btn'
|
||||
error={!!errors.btn_id}
|
||||
helperText={errors.btn_id?.message}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
) : (
|
||||
<>
|
||||
<FormItem
|
||||
label='按钮位置'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_position'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value='top_left'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>左上</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='top_right'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>右上</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='bottom_left'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>左下</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='bottom_right'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>右下</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
{btn_style !== 'hover_ball' && (
|
||||
<FormItem
|
||||
label='按钮文字'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_text'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
placeholder='输入按钮文字'
|
||||
error={!!errors.btn_text}
|
||||
helperText={errors.btn_text?.message}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
<FormItem
|
||||
label='按钮图标'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_logo'
|
||||
render={({ field }) => (
|
||||
<UploadFile
|
||||
{...field}
|
||||
id='btn_logo'
|
||||
type='url'
|
||||
accept='image/*'
|
||||
width={80}
|
||||
onChange={url => {
|
||||
field.onChange(url);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='弹框配置'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Box>
|
||||
{!modalConfigOpen && (
|
||||
<Button
|
||||
size='small'
|
||||
variant='outlined'
|
||||
onClick={() => setModalConfigOpen(true)}
|
||||
endIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
展开
|
||||
</Button>
|
||||
)}
|
||||
<Collapse in={modalConfigOpen}>
|
||||
<Stack spacing={2.5}>
|
||||
{/* <FormItem label='配色方案' sx={{ alignItems: 'flex-start' }} labelSx={{ mt: 1 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='theme_mode'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value='light'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>浅色模式</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='dark'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>深色模式</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='system'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>跟随系统</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='wiki'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>跟随 WIKI 网站</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem> */}
|
||||
<FormItem
|
||||
label='弹窗位置'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='modal_position'
|
||||
render={({ field }) => {
|
||||
const isDisabled = btn_style === 'btn_trigger';
|
||||
return (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
value={isDisabled ? 'fixed' : field.value}
|
||||
onChange={e => {
|
||||
if (!isDisabled) {
|
||||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value='follow'
|
||||
control={
|
||||
<Radio size='small' disabled={isDisabled} />
|
||||
}
|
||||
label={<Box sx={{ width: 100 }}>跟随按钮</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='fixed'
|
||||
control={
|
||||
<Radio size='small' disabled={isDisabled} />
|
||||
}
|
||||
label={<Box sx={{ width: 100 }}>居中展示</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='搜索模式'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='search_mode'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value='all'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>双模式切换</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='qa'
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
<Box sx={{ width: 100 }}>智能问答模式</Box>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='doc'
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
<Box sx={{ width: 100 }}>搜索文档模式</Box>
|
||||
}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='搜索提示语'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='placeholder'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
{...field}
|
||||
placeholder='输入搜索提示语'
|
||||
error={!!errors.placeholder}
|
||||
helperText={errors.placeholder?.message}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='推荐问题'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
{...recommendQuestionsField}
|
||||
placeholder='回车确认,填写下一个推荐问题'
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='推荐文档'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<RecommendDocDragList
|
||||
ids={recommend_node_ids}
|
||||
onChange={(value: string[]) => {
|
||||
setIsEdit(true);
|
||||
setValue('recommend_node_ids', value);
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='免责声明'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='disclaimer'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
{...field}
|
||||
placeholder='输入免责声明'
|
||||
error={!!errors.disclaimer}
|
||||
helperText={errors.disclaimer?.message}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</SettingCardItem>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { DomainKnowledgeBaseDetail } from '@/request/types';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Link,
|
||||
|
|
@ -13,10 +12,11 @@ import {
|
|||
import ShowText from '@/components/ShowText';
|
||||
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormItem, SettingCardItem } from './Common';
|
||||
import { DomainAppDetailResp } from '@/request/types';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
const CardRobotApi = ({
|
||||
|
|
@ -29,11 +29,6 @@ const CardRobotApi = ({
|
|||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
|
|
@ -114,10 +109,7 @@ const CardRobotApi = ({
|
|||
}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<FormItem
|
||||
label='问答机器人 API'
|
||||
tooltip={!isEnterprise ? '企业版可用' : undefined}
|
||||
>
|
||||
<FormItem label='问答机器人 API' permission={BUSINESS_VERSION_PERMISSION}>
|
||||
<FormControl>
|
||||
<Controller
|
||||
control={control}
|
||||
|
|
@ -133,13 +125,11 @@ const CardRobotApi = ({
|
|||
<Stack direction={'row'}>
|
||||
<FormControlLabel
|
||||
value={true}
|
||||
disabled={!isEnterprise}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>启用</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={false}
|
||||
disabled={!isEnterprise}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>禁用</Box>}
|
||||
/>
|
||||
|
|
@ -150,7 +140,7 @@ const CardRobotApi = ({
|
|||
</FormControl>
|
||||
</FormItem>
|
||||
|
||||
{isEnabled && (
|
||||
{isEnabled && BUSINESS_VERSION_PERMISSION.includes(license.edition!) && (
|
||||
<>
|
||||
<FormItem label='API Token' required>
|
||||
<Controller
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import {
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { FormItem, SettingCardItem } from './Common';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
const CardRobotWecomService = ({
|
||||
kb,
|
||||
|
|
@ -262,38 +264,40 @@ const CardRobotWecomService = ({
|
|||
<Icon type='icon-jinggao' sx={{ fontSize: 18 }} />
|
||||
人工客服转接配置:当用户触发以下场景时,会自动转接人工客服
|
||||
</Stack>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
包含特定
|
||||
<VersionMask permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
包含特定
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...containKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
完全匹配
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...containKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
完全匹配
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...equalKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...equalKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
</VersionMask>
|
||||
</>
|
||||
)}
|
||||
</SettingCardItem>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from '@/request/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
|
|
@ -17,7 +18,7 @@ import {
|
|||
TextField,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { FormItem, SettingCardItem } from './Common';
|
||||
|
||||
|
|
@ -32,15 +33,9 @@ const WatermarkForm = ({
|
|||
data?: DomainAppDetailResp;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const [watermarkIsEdit, setWatermarkIsEdit] = useState(false);
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
const { control, handleSubmit, setValue, watch } = useForm({
|
||||
defaultValues: {
|
||||
watermark_setting: data?.settings?.watermark_setting ?? null,
|
||||
watermark_content: data?.settings?.watermark_content ?? '',
|
||||
|
|
@ -48,9 +43,6 @@ const WatermarkForm = ({
|
|||
});
|
||||
|
||||
const watermarkSetting = watch('watermark_setting');
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const handleSaveWatermark = handleSubmit(values => {
|
||||
if (!data?.id || values.watermark_setting === null) return;
|
||||
|
|
@ -82,8 +74,9 @@ const WatermarkForm = ({
|
|||
title='水印'
|
||||
isEdit={watermarkIsEdit}
|
||||
onSubmit={handleSaveWatermark}
|
||||
permission={BUSINESS_VERSION_PERMISSION}
|
||||
>
|
||||
<FormItem label='水印开关' tooltip={!isEnterprise && '企业版可用'}>
|
||||
<FormItem label='水印开关'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='watermark_setting'
|
||||
|
|
@ -98,18 +91,18 @@ const WatermarkForm = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={ConstsWatermarkSetting.WatermarkVisible}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>显性水印</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={ConstsWatermarkSetting.WatermarkHidden}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>隐形水印</StyledRadioLabel>}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
value={ConstsWatermarkSetting.WatermarkDisabled}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
@ -128,7 +121,6 @@ const WatermarkForm = ({
|
|||
placeholder='请输入水印内容, 支持多行输入'
|
||||
multiline
|
||||
minRows={2}
|
||||
disabled={!isEnterprise}
|
||||
onChange={e => {
|
||||
setWatermarkIsEdit(true);
|
||||
field.onChange(e.target.value);
|
||||
|
|
@ -146,9 +138,6 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
|||
const { license } = useAppSelector(state => state.config);
|
||||
const [questionInputValue, setQuestionInputValue] = useState('');
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
|
|
@ -169,17 +158,18 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb.id || !isEnterprise) return;
|
||||
if (!kb.id || !BUSINESS_VERSION_PERMISSION.includes(license.edition!))
|
||||
return;
|
||||
getApiProV1Block({ kb_id: kb.id! }).then(res => {
|
||||
setValue('block_words', res.words || []);
|
||||
});
|
||||
}, [kb, isEnterprise]);
|
||||
}, [kb, license.edition]);
|
||||
|
||||
return (
|
||||
<SettingCardItem title='内容合规' isEdit={isEdit} onSubmit={onSubmit}>
|
||||
<FormItem
|
||||
vertical
|
||||
tooltip={!isEnterprise && '企业版可用'}
|
||||
permission={BUSINESS_VERSION_PERMISSION}
|
||||
label='屏蔽 AI 问答中的关键字'
|
||||
>
|
||||
<Controller
|
||||
|
|
@ -193,7 +183,6 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
|||
inputValue={questionInputValue}
|
||||
options={[]}
|
||||
fullWidth
|
||||
disabled={!isEnterprise}
|
||||
onInputChange={(_, value) => {
|
||||
setQuestionInputValue(value);
|
||||
}}
|
||||
|
|
@ -234,23 +223,14 @@ const CopyForm = ({
|
|||
data?: DomainAppDetailResp;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
copy_setting: data?.settings?.copy_setting ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const handleSaveWatermark = handleSubmit(values => {
|
||||
if (!data?.id || values.copy_setting === null) return;
|
||||
putApiV1App(
|
||||
|
|
@ -280,7 +260,7 @@ const CopyForm = ({
|
|||
isEdit={isEdit}
|
||||
onSubmit={handleSaveWatermark}
|
||||
>
|
||||
<FormItem label='限制复制' tooltip={!isEnterprise && '企业版可用'}>
|
||||
<FormItem label='限制复制' permission={BUSINESS_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='copy_setting'
|
||||
|
|
@ -295,18 +275,18 @@ const CopyForm = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={ConstsCopySetting.CopySettingNone}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>不做限制</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={ConstsCopySetting.CopySettingAppend}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>增加内容尾巴</StyledRadioLabel>}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
value={ConstsCopySetting.CopySettingDisabled}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁止复制内容</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
|||
defaultValues: {
|
||||
desc: '',
|
||||
keyword: '',
|
||||
auto_sitemap: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -44,7 +43,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
|||
useEffect(() => {
|
||||
setValue('desc', data.settings?.desc || '');
|
||||
setValue('keyword', data.settings?.keyword || '');
|
||||
setValue('auto_sitemap', data.settings?.auto_sitemap ?? false);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
|
|
@ -88,25 +86,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
|||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label='自动生成 Sitemap'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='auto_sitemap'
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
size='small'
|
||||
sx={{ p: 0, m: 0 }}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
</SettingCardItem>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import Card from '@/components/Card';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { Button, Stack, styled, SxProps, Tooltip } from '@mui/material';
|
||||
import { createContext, useContext } from 'react';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
|
||||
const StyledForm = styled('form')<{ gap?: number | string }>(
|
||||
({ theme, gap = 2 }) => ({
|
||||
|
|
@ -40,6 +42,7 @@ const StyledFormLabel = styled('span')<{ required?: boolean }>(
|
|||
|
||||
export const StyledFormItem = styled('div')<{ vertical?: boolean }>(
|
||||
({ theme, vertical }) => ({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: vertical ? 'flex-start' : 'center',
|
||||
flexDirection: vertical ? 'column' : 'row',
|
||||
|
|
@ -82,6 +85,7 @@ export const FormItem = ({
|
|||
extra,
|
||||
sx,
|
||||
labelSx,
|
||||
permission,
|
||||
}: {
|
||||
label?: string | React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
|
|
@ -92,31 +96,37 @@ export const FormItem = ({
|
|||
extra?: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
labelSx?: SxProps;
|
||||
permission?: number[];
|
||||
}) => {
|
||||
const { vertical: verticalContext, labelWidth: labelWidthContext } =
|
||||
useContext(FormContext);
|
||||
return (
|
||||
<StyledFormItem vertical={vertical || verticalContext} sx={sx}>
|
||||
<StyledFormLabelWrapper
|
||||
vertical={vertical || verticalContext}
|
||||
labelWidth={labelWidth || labelWidthContext}
|
||||
sx={labelSx}
|
||||
>
|
||||
<Stack direction='row' alignItems='center' flex={1}>
|
||||
<StyledFormLabel required={required}>{label}</StyledFormLabel>
|
||||
{tooltip && typeof tooltip === 'string' ? (
|
||||
<Tooltip title={tooltip} placement='top' arrow>
|
||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
tooltip
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{extra}
|
||||
</StyledFormLabelWrapper>
|
||||
{children}
|
||||
</StyledFormItem>
|
||||
return (
|
||||
<VersionMask permission={permission}>
|
||||
<StyledFormItem vertical={vertical || verticalContext} sx={sx}>
|
||||
<StyledFormLabelWrapper
|
||||
vertical={vertical || verticalContext}
|
||||
labelWidth={labelWidth || labelWidthContext}
|
||||
sx={labelSx}
|
||||
>
|
||||
<Stack direction='row' alignItems='center' flex={1}>
|
||||
<StyledFormLabel required={required}>{label}</StyledFormLabel>
|
||||
{tooltip && typeof tooltip === 'string' ? (
|
||||
<Tooltip title={tooltip} placement='top' arrow>
|
||||
<InfoIcon
|
||||
sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
tooltip
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{extra}
|
||||
</StyledFormLabelWrapper>
|
||||
{children}
|
||||
</StyledFormItem>
|
||||
</VersionMask>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -142,6 +152,7 @@ export const SettingCard = ({
|
|||
};
|
||||
|
||||
const StyledSettingCardItem = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
'&:not(:last-child)': {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
paddingBottom: theme.spacing(4),
|
||||
|
|
@ -204,6 +215,12 @@ export const SettingCardItem = ({
|
|||
extra,
|
||||
more,
|
||||
sx,
|
||||
permission = [
|
||||
ConstsLicenseEdition.LicenseEditionFree,
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
],
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
|
|
@ -212,6 +229,7 @@ export const SettingCardItem = ({
|
|||
extra?: React.ReactNode;
|
||||
more?: SettingCardItemMore;
|
||||
sx?: SxProps;
|
||||
permission?: number[];
|
||||
}) => {
|
||||
const renderMore = (more: SettingCardItemMore) => {
|
||||
if (more && typeof more === 'object' && 'type' in more) {
|
||||
|
|
@ -237,20 +255,23 @@ export const SettingCardItem = ({
|
|||
return more;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledSettingCardItem sx={sx}>
|
||||
<StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemTitle>
|
||||
{title} {renderMore(more)}
|
||||
</StyledSettingCardItemTitle>
|
||||
{isEdit && (
|
||||
<Button variant='contained' size='small' onClick={onSubmit}>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
{extra}
|
||||
</StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemContent>{children}</StyledSettingCardItemContent>
|
||||
</StyledSettingCardItem>
|
||||
<VersionMask permission={permission}>
|
||||
<StyledSettingCardItem sx={sx}>
|
||||
<StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemTitle>
|
||||
{title} {renderMore(more)}
|
||||
</StyledSettingCardItemTitle>
|
||||
{isEdit && (
|
||||
<Button variant='contained' size='small' onClick={onSubmit}>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
{extra}
|
||||
</StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemContent>{children}</StyledSettingCardItemContent>
|
||||
</StyledSettingCardItem>
|
||||
</VersionMask>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SettingCardItem } from '../Common';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { Modal, message } from '@ctzhian/ui';
|
||||
import { Stack, Button } from '@mui/material';
|
||||
import { Box } from '@mui/material';
|
||||
import { postApiProV1AuthGroupSync } from '@/request/pro/AuthOrg';
|
||||
import {
|
||||
|
|
@ -19,6 +16,7 @@ import {
|
|||
deleteApiProV1AuthGroupDelete,
|
||||
} from '@/request/pro/AuthGroup';
|
||||
import GroupTree from './GroupTree';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface UserGroupProps {
|
||||
enabled: string;
|
||||
|
|
@ -45,10 +43,6 @@ const UserGroup = ({
|
|||
GithubComChaitinPandaWikiProApiAuthV1AuthGroupTreeItem[]
|
||||
>([]);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const onDeleteUserGroup = (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '删除用户组',
|
||||
|
|
@ -74,10 +68,15 @@ const UserGroup = ({
|
|||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!kb_id || enabled !== '2' || !isEnterprise) return;
|
||||
if (
|
||||
!kb_id ||
|
||||
enabled !== '2' ||
|
||||
!BUSINESS_VERSION_PERMISSION.includes(license.edition!)
|
||||
)
|
||||
return;
|
||||
getUserGroup();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [kb_id, enabled, isEnterprise]);
|
||||
}, [kb_id, enabled, license.edition!]);
|
||||
|
||||
const handleMove = async ({
|
||||
id,
|
||||
|
|
@ -123,32 +122,7 @@ const UserGroup = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<SettingCardItem
|
||||
title='用户组'
|
||||
more={
|
||||
!isEnterprise && (
|
||||
<Tooltip title='企业版可用' placement='top' arrow>
|
||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
// extra={
|
||||
// isEnterprise &&
|
||||
// [
|
||||
// ConstsSourceType.SourceTypeWeCom,
|
||||
// ConstsSourceType.SourceTypeDingTalk,
|
||||
// ].includes(sourceType as ConstsSourceType) && (
|
||||
// <Button
|
||||
// color='primary'
|
||||
// size='small'
|
||||
// onClick={handleSync}
|
||||
// loading={syncLoading}
|
||||
// >
|
||||
// 同步组织架构和成员
|
||||
// </Button>
|
||||
// )
|
||||
// }
|
||||
>
|
||||
<SettingCardItem title='用户组' permission={BUSINESS_VERSION_PERMISSION}>
|
||||
<Box
|
||||
sx={{
|
||||
border: '1px dashed',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ import QAReferer from './QAReferer';
|
|||
import RTVisitor from './RTVisitor';
|
||||
import TypeCount from './TypeCount';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import {
|
||||
BUSINESS_VERSION_PERMISSION,
|
||||
PROFESSION_VERSION_PERMISSION,
|
||||
} from '@/constant/version';
|
||||
|
||||
export const TimeList = [
|
||||
{ label: '近 24 小时', value: 1 },
|
||||
|
|
@ -25,13 +30,40 @@ const Statistic = () => {
|
|||
const isWideScreen = useMediaQuery('(min-width:1190px)');
|
||||
|
||||
const timeList = useMemo(() => {
|
||||
const isPro = license.edition === 1 || license.edition === 2;
|
||||
const isEnterprise = license.edition === 2;
|
||||
const isPro = PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
const isBusiness = BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
return [
|
||||
{ label: '近 24 小时', value: 1, disabled: false },
|
||||
{ label: '近 7 天', value: 7, disabled: !isPro },
|
||||
{ label: '近 30 天', value: 30, disabled: !isEnterprise },
|
||||
{ label: '近 90 天', value: 90, disabled: !isEnterprise },
|
||||
{
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||
<span>近 7 天</span>
|
||||
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: 7,
|
||||
disabled: !isPro,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||
<span>近 30 天</span>
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: 30,
|
||||
disabled: !isBusiness,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||
<span>近 90 天</span>
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: 90,
|
||||
disabled: !isBusiness,
|
||||
},
|
||||
];
|
||||
}, [license]);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import {
|
|||
GetApiV1NodeListParams,
|
||||
GetApiV1NodeRecommendNodesParams,
|
||||
V1NodeDetailResp,
|
||||
V1NodeRestudyReq,
|
||||
V1NodeRestudyResp,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
|
|
@ -263,6 +265,38 @@ export const getApiV1NodeRecommendNodes = (
|
|||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 文档重新学习
|
||||
*
|
||||
* @tags Node
|
||||
* @name PostApiV1NodeRestudy
|
||||
* @summary 文档重新学习
|
||||
* @request POST:/api/v1/node/restudy
|
||||
* @secure
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: V1NodeRestudyResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postApiV1NodeRestudy = (
|
||||
param: V1NodeRestudyReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: V1NodeRestudyResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/node/restudy`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Summary Node
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import httpRequest, { ContentType, RequestParams } from "./httpClient";
|
||||
import { DomainResponse, V1NodeRestudyReq, V1NodeRestudyResp } from "./types";
|
||||
|
||||
/**
|
||||
* @description 文档重新学习
|
||||
*
|
||||
* @tags NodeRestudy
|
||||
* @name PostApiV1NodeRestudy
|
||||
* @summary 文档重新学习
|
||||
* @request POST:/api/v1/node/restudy
|
||||
* @secure
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: V1NodeRestudyResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postApiV1NodeRestudy = (
|
||||
param: V1NodeRestudyReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: V1NodeRestudyResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/node/restudy`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
|
@ -10,7 +10,6 @@ export * from './Message'
|
|||
export * from './Model'
|
||||
export * from './Node'
|
||||
export * from './NodePermission'
|
||||
export * from './NodeRestudy'
|
||||
export * from './Stat'
|
||||
export * from './User'
|
||||
export * from './types'
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
||||
|
|
@ -206,6 +207,32 @@ export const postShareProV1AuthLdap = (
|
|||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 用户登出
|
||||
*
|
||||
* @tags ShareAuth
|
||||
* @name PostShareProV1AuthLogout
|
||||
* @summary 用户登出
|
||||
* @request POST:/share/pro/v1/auth/logout
|
||||
* @response `200` `(DomainPWResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postShareProV1AuthLogout = (params: RequestParams = {}) =>
|
||||
httpRequest<
|
||||
DomainPWResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp;
|
||||
}
|
||||
>({
|
||||
path: `/share/pro/v1/auth/logout`,
|
||||
method: "POST",
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description OAuth登录
|
||||
*
|
||||
|
|
|
|||
|
|
@ -52,10 +52,12 @@ export enum ConstsSourceType {
|
|||
export enum ConstsLicenseEdition {
|
||||
/** 开源版 */
|
||||
LicenseEditionFree = 0,
|
||||
/** 联创版 */
|
||||
LicenseEditionContributor = 1,
|
||||
/** 专业版 */
|
||||
LicenseEditionProfession = 1,
|
||||
/** 企业版 */
|
||||
LicenseEditionEnterprise = 2,
|
||||
/** 商业版 */
|
||||
LicenseEditionBusiness = 3,
|
||||
}
|
||||
|
||||
export enum ConstsContributeType {
|
||||
|
|
@ -455,6 +457,11 @@ export type GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp = Record<
|
|||
any
|
||||
>;
|
||||
|
||||
export type GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp = Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
||||
kb_id?: string;
|
||||
redirect_url?: string;
|
||||
|
|
@ -465,6 +472,7 @@ export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp {
|
|||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthWecomReq {
|
||||
is_app?: boolean;
|
||||
kb_id?: string;
|
||||
redirect_url?: string;
|
||||
}
|
||||
|
|
@ -668,8 +676,6 @@ export interface GetApiProV1TokenListParams {
|
|||
}
|
||||
|
||||
export interface PostApiV1LicensePayload {
|
||||
/** license edition */
|
||||
license_edition: "contributor" | "enterprise";
|
||||
/** license type */
|
||||
license_type: "file" | "code";
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -171,14 +171,21 @@ export enum ConstsNodeAccessPerm {
|
|||
NodeAccessPermClosed = "closed",
|
||||
}
|
||||
|
||||
export enum ConstsModelSettingMode {
|
||||
ModelSettingModeManual = "manual",
|
||||
ModelSettingModeAuto = "auto",
|
||||
}
|
||||
|
||||
/** @format int32 */
|
||||
export enum ConstsLicenseEdition {
|
||||
/** 开源版 */
|
||||
LicenseEditionFree = 0,
|
||||
/** 联创版 */
|
||||
LicenseEditionContributor = 1,
|
||||
/** 专业版 */
|
||||
LicenseEditionProfession = 1,
|
||||
/** 企业版 */
|
||||
LicenseEditionEnterprise = 2,
|
||||
/** 商业版 */
|
||||
LicenseEditionBusiness = 3,
|
||||
}
|
||||
|
||||
export enum ConstsHomePageSetting {
|
||||
|
|
@ -927,8 +934,10 @@ export interface DomainModelModeSetting {
|
|||
auto_mode_api_key?: string;
|
||||
/** 自定义对话模型名称 */
|
||||
chat_model?: string;
|
||||
/** 手动模式下嵌入模型是否更新 */
|
||||
is_manual_embedding_updated?: boolean;
|
||||
/** 模式: manual 或 auto */
|
||||
mode?: string;
|
||||
mode?: ConstsModelSettingMode;
|
||||
}
|
||||
|
||||
export interface DomainMoveNodeReq {
|
||||
|
|
@ -1180,6 +1189,17 @@ export interface DomainShareConversationMessage {
|
|||
role?: SchemaRoleType;
|
||||
}
|
||||
|
||||
export interface DomainShareNodeListItemResp {
|
||||
emoji?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
parent_id?: string;
|
||||
permissions?: DomainNodePermissions;
|
||||
position?: number;
|
||||
type?: DomainNodeType;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface DomainSimpleAuth {
|
||||
enabled?: boolean;
|
||||
password?: string;
|
||||
|
|
@ -1357,11 +1377,18 @@ export interface DomainWecomAIBotSettings {
|
|||
}
|
||||
|
||||
export interface DomainWidgetBotSettings {
|
||||
btn_id?: string;
|
||||
btn_logo?: string;
|
||||
btn_position?: string;
|
||||
btn_style?: string;
|
||||
btn_text?: string;
|
||||
disclaimer?: string;
|
||||
is_open?: boolean;
|
||||
modal_position?: string;
|
||||
placeholder?: string;
|
||||
recommend_node_ids?: string[];
|
||||
recommend_questions?: string[];
|
||||
search_mode?: string;
|
||||
theme_mode?: string;
|
||||
}
|
||||
|
||||
|
|
@ -1645,8 +1672,9 @@ export interface V1NodePermissionResp {
|
|||
}
|
||||
|
||||
export interface V1NodeRestudyReq {
|
||||
kb_id?: string;
|
||||
node_ids?: string[];
|
||||
kb_id: string;
|
||||
/** @minItems 1 */
|
||||
node_ids: string[];
|
||||
}
|
||||
|
||||
export type V1NodeRestudyResp = Record<string, any>;
|
||||
|
|
@ -1666,6 +1694,7 @@ export interface V1ShareNodeDetailResp {
|
|||
editor_id?: string;
|
||||
id?: string;
|
||||
kb_id?: string;
|
||||
list?: DomainShareNodeListItemResp[];
|
||||
meta?: DomainNodeMeta;
|
||||
name?: string;
|
||||
parent_id?: string;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:20-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export default isDevelopment
|
|||
|
||||
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||
// This can increase your server load as well as your hosting bill.
|
||||
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||
// Note: Check that the configured route will not match with your Next.js proxy, otherwise reporting of client-
|
||||
// side errors will fail.
|
||||
tunnelRoute: '/monitoring',
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"version": "2.9.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 3010",
|
||||
"dev": "next dev -p 3010",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@mui/material-nextjs": "^7.1.0",
|
||||
"@mui/material-nextjs": "^7.3.5",
|
||||
"@sentry/nextjs": "^10.8.0",
|
||||
"@types/markdown-it": "13.0.1",
|
||||
"@vscode/markdown-it-katex": "^1.1.2",
|
||||
|
|
@ -25,12 +25,13 @@
|
|||
"highlight.js": "^11.11.1",
|
||||
"html-react-parser": "^5.2.5",
|
||||
"html-to-image": "^1.11.13",
|
||||
"import-in-the-middle": "^1.4.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"katex": "^0.16.22",
|
||||
"markdown-it": "13.0.1",
|
||||
"markdown-it-highlightjs": "^4.2.0",
|
||||
"mermaid": "^11.9.0",
|
||||
"next": "15.4.6",
|
||||
"next": "^16.0.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-photo-view": "^1.2.7",
|
||||
|
|
@ -41,17 +42,23 @@
|
|||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"require-in-the-middle": "^7.5.2",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ctzhian/cx-swagger-api": "^1.0.0",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@next/eslint-plugin-next": "^15.4.5",
|
||||
"@next/eslint-plugin-next": "^16.0.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/rangy": "^1.3.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"eslint-config-next": "15.3.2",
|
||||
"eslint-config-next": "16.0.0",
|
||||
"eslint-config-prettier": "^9.1.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
|
||||
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"require-in-the-middle": "^7.5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,39 @@
|
|||
/* 挂件按钮样式 - 基于MUI主题 */
|
||||
/* 挂件按钮基础样式 */
|
||||
.widget-bot-button {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 190px;
|
||||
z-index: 9999;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border-radius: 18px 0 0 18px;
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0px 6px 10px 0px rgba(54, 59, 76, 0.17);
|
||||
padding: 11px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
|
||||
border: none;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
/* 优化拖拽性能 */
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.widget-bot-button:hover {
|
||||
.widget-bot-button:hover:not(.dragging) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(50, 72, 242, 0.2);
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball:hover:not(.dragging) {
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
||||
.widget-bot-button.dragging {
|
||||
cursor: grabbing;
|
||||
transform: rotate(2deg);
|
||||
box-shadow: 0 6px 12px rgba(50, 72, 242, 0.25);
|
||||
transition: none !important;
|
||||
/* 拖拽时禁用过渡,提升性能 */
|
||||
/* transform 由 JS 控制,包含 rotate 和 translate */
|
||||
}
|
||||
|
||||
.widget-bot-button-content {
|
||||
|
|
@ -39,14 +43,13 @@
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
.widget-bot-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-bottom: 8px;
|
||||
/* 图标样式 */
|
||||
.widget-bot-icon {
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 文字样式 */
|
||||
.widget-bot-text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
|
@ -60,6 +63,47 @@
|
|||
margin: 1px 0;
|
||||
}
|
||||
|
||||
/* 侧边吸附按钮样式 */
|
||||
.widget-bot-side-sticky {
|
||||
width: 48px;
|
||||
padding: 6px 6px 12px 6px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
||||
border-radius: 24px;
|
||||
border: 1px solid #ECEEF1;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.widget-bot-side-sticky .widget-bot-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.widget-bot-side-sticky .widget-bot-text {
|
||||
font-size: 12px;
|
||||
color: #646a73;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
/* 悬浮球按钮样式 */
|
||||
.widget-bot-hover-ball {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
||||
border: 1px solid #ECEEF1;
|
||||
padding: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball .widget-bot-hover-ball-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 模态框样式 - 基于MUI主题 */
|
||||
.widget-bot-modal {
|
||||
position: fixed;
|
||||
|
|
@ -75,6 +119,11 @@
|
|||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.widget-bot-modal-fixed {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.widget-bot-modal-content {
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
|
|
@ -88,6 +137,14 @@
|
|||
animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.widget-bot-modal-content-fixed {
|
||||
position: relative;
|
||||
width: 800px;
|
||||
height: auto;
|
||||
max-height: 90vh;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
|
@ -100,34 +157,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* 关闭按钮样式 - 基于MUI IconButton */
|
||||
/* 关闭按钮样式 - 透明框 */
|
||||
.widget-bot-close-btn {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: none;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
top: 22.5px;
|
||||
right: 16px;
|
||||
background: transparent;
|
||||
width: 36.26px;
|
||||
height: 25px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
border-radius: 0;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
opacity: 0.5;
|
||||
z-index: 10001;
|
||||
transition: all 0.1s ease-in-out;
|
||||
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||
}
|
||||
|
||||
.widget-bot-close-btn:hover {
|
||||
font-size: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.widget-bot-close-btn:active {
|
||||
transform: scale(0.95);
|
||||
z-index: 10001;
|
||||
transition: none;
|
||||
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
/* 允许鼠标穿透到下方 */
|
||||
}
|
||||
|
||||
/* iframe样式 */
|
||||
|
|
@ -140,6 +193,11 @@
|
|||
background: #F8F9FA;
|
||||
}
|
||||
|
||||
.widget-bot-modal-content-fixed .widget-bot-iframe {
|
||||
min-height: 600px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* 防止页面滚动 */
|
||||
body.widget-bot-modal-open {
|
||||
overflow: hidden;
|
||||
|
|
@ -147,19 +205,34 @@ body.widget-bot-modal-open {
|
|||
|
||||
/* 暗色主题支持 - 基于data-theme属性 */
|
||||
.widget-bot-button[data-theme="dark"] {
|
||||
background: #6E73FE;
|
||||
box-shadow: 0 2px 4px rgba(110, 115, 254, 0.15);
|
||||
}
|
||||
|
||||
.widget-bot-button[data-theme="dark"]:hover {
|
||||
.widget-bot-side-sticky[data-theme="dark"] {
|
||||
background: #6E73FE;
|
||||
}
|
||||
|
||||
.widget-bot-side-sticky[data-theme="dark"]:hover {
|
||||
background: #5d68fd;
|
||||
box-shadow: 0 4px 8px rgba(110, 115, 254, 0.2);
|
||||
}
|
||||
|
||||
.widget-bot-button[data-theme="dark"].dragging {
|
||||
.widget-bot-side-sticky[data-theme="dark"].dragging {
|
||||
box-shadow: 0 6px 12px rgba(110, 115, 254, 0.25);
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="dark"] {
|
||||
background: #6E73FE;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="dark"]:hover {
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="dark"].dragging {
|
||||
box-shadow: 0 8px 20px rgba(110, 115, 254, 0.3);
|
||||
}
|
||||
|
||||
.widget-bot-modal[data-theme="dark"] {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
|
@ -169,61 +242,63 @@ body.widget-bot-modal-open {
|
|||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
/* 移动端适配 - 统一处理 */
|
||||
@media (max-width: 768px) {
|
||||
.widget-bot-button {
|
||||
bottom: 16px;
|
||||
padding: 8px;
|
||||
border-radius: 10px 0 0 10px;
|
||||
.widget-bot-side-sticky {
|
||||
width: 48px;
|
||||
padding: 6px 6px 12px 6px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball .widget-bot-hover-ball-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.widget-bot-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.widget-bot-logo {
|
||||
.widget-bot-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* 移动端弹框统一居中显示,宽度100%-32px,高度90vh */
|
||||
.widget-bot-modal-content {
|
||||
width: calc(100% - 60.5px);
|
||||
height: 90%;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
position: relative !important;
|
||||
width: calc(100% - 32px) !important;
|
||||
height: 90vh !important;
|
||||
max-width: none !important;
|
||||
max-height: 90vh !important;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
margin: auto !important;
|
||||
}
|
||||
|
||||
.widget-bot-modal-content-fixed {
|
||||
width: calc(100% - 32px) !important;
|
||||
height: 90vh !important;
|
||||
max-height: 90vh !important;
|
||||
}
|
||||
|
||||
.widget-bot-close-btn {
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏幕适配 */
|
||||
@media (max-width: 480px) {
|
||||
.widget-bot-button {
|
||||
bottom: 12px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.widget-bot-text {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.widget-bot-modal-content {
|
||||
width: calc(100% - 55.5px);
|
||||
height: 90%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.widget-bot-close-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 14px;
|
||||
top: 22.5px;
|
||||
right: 16px;
|
||||
width: 36.26px;
|
||||
height: 25px;
|
||||
font-size: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -274,19 +349,32 @@ body.widget-bot-modal-open {
|
|||
}
|
||||
|
||||
/* 浅色主题样式 - 显式定义 */
|
||||
.widget-bot-button[data-theme="light"] {
|
||||
background: #3248F2;
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0px 6px 10px 0px rgba(54, 59, 76, 0.17);
|
||||
.widget-bot-side-sticky[data-theme="light"] {
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
||||
border: 1px solid #ECEEF1;
|
||||
}
|
||||
|
||||
.widget-bot-button[data-theme="light"]:hover {
|
||||
background: #2a3cdb;
|
||||
box-shadow: 0 4px 8px rgba(50, 72, 242, 0.2);
|
||||
.widget-bot-side-sticky[data-theme="light"]:hover {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.widget-bot-button[data-theme="light"].dragging {
|
||||
box-shadow: 0 6px 12px rgba(50, 72, 242, 0.25);
|
||||
.widget-bot-side-sticky[data-theme="light"].dragging {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="light"] {
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
||||
border: 1px solid #ECEEF1;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="light"]:hover {
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="light"].dragging {
|
||||
box-shadow: 0 8px 20px rgba(50, 72, 242, 0.3);
|
||||
}
|
||||
|
||||
.widget-bot-modal[data-theme="light"] {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
const defaultModalPosition = 'follow';
|
||||
const defaultBtnPosition = 'bottom_left';
|
||||
const defaultBtnStyle = 'side_sticky';
|
||||
|
||||
// 获取当前脚本的域名
|
||||
const currentScript = document.currentScript || document.querySelector('script[src*="widget-bot.js"]');
|
||||
const widgetDomain = currentScript ? new URL(currentScript.src).origin : window.location.origin;
|
||||
|
|
@ -11,6 +15,13 @@
|
|||
let isDragging = false;
|
||||
let dragOffset = { x: 0, y: 0 };
|
||||
let currentTheme = 'light'; // 默认浅色主题
|
||||
let customTriggerElement = null; // 自定义触发元素
|
||||
let customTriggerHandler = null; // 自定义触发元素的事件处理函数
|
||||
let dragAnimationFrame = null; // 拖拽动画帧ID
|
||||
let buttonSize = { width: 0, height: 0 }; // 缓存按钮尺寸
|
||||
let initialPosition = { left: 0, top: 0 }; // 拖拽开始时的初始位置
|
||||
let hasDragged = false; // 标记是否发生了拖拽
|
||||
let dragStartPos = { x: 0, y: 0 }; // 拖拽开始时的鼠标位置
|
||||
|
||||
// 应用主题
|
||||
function applyTheme(theme_mode) {
|
||||
|
|
@ -60,13 +71,22 @@
|
|||
applyTheme(widgetInfo.theme_mode);
|
||||
}
|
||||
|
||||
createWidget();
|
||||
// 根据 btn_style 创建不同的挂件
|
||||
const btnStyle = widgetInfo.btn_style || defaultBtnStyle;
|
||||
if (btnStyle === 'btn_trigger') {
|
||||
createCustomTrigger();
|
||||
} else {
|
||||
createWidget();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取挂件信息失败:', error);
|
||||
// 使用默认值
|
||||
widgetInfo = {
|
||||
btn_text: '在线客服',
|
||||
btn_logo: '',
|
||||
btn_logo: `''`,
|
||||
btn_style: defaultBtnStyle,
|
||||
btn_position: defaultBtnPosition,
|
||||
modal_position: defaultModalPosition,
|
||||
theme_mode: 'light'
|
||||
};
|
||||
applyTheme(widgetInfo.theme_mode);
|
||||
|
|
@ -78,53 +98,92 @@
|
|||
}
|
||||
}
|
||||
|
||||
// 创建垂直文字
|
||||
function createVerticalText(text) {
|
||||
return text.split('').map((char, index) =>
|
||||
`<span>${char}</span>`
|
||||
).join('');
|
||||
// 创建两行文字(每行两个字)
|
||||
function createTwoLineText(text) {
|
||||
const chars = text.split('').filter(it => !!it.trim());
|
||||
const lines = [];
|
||||
for (let i = 0; i < chars.length; i += 2) {
|
||||
lines.push(chars.slice(i, i + 2).join(''));
|
||||
}
|
||||
return lines.map(line => `<span>${line}</span>`).join('');
|
||||
}
|
||||
|
||||
// 创建挂件按钮
|
||||
function createWidget() {
|
||||
// 如果已存在,先删除
|
||||
if (widgetButton) {
|
||||
widgetButton.remove();
|
||||
}
|
||||
// 应用按钮位置
|
||||
function applyButtonPosition(button, position) {
|
||||
const pos = position || defaultBtnPosition;
|
||||
button.style.top = 'auto';
|
||||
button.style.right = 'auto';
|
||||
button.style.bottom = 'auto';
|
||||
button.style.left = 'auto';
|
||||
|
||||
// 创建按钮容器
|
||||
// 两种模式使用相同的默认位置:距离边缘16px,垂直方向190px
|
||||
switch (pos) {
|
||||
case 'top_left':
|
||||
button.style.top = '190px';
|
||||
button.style.left = '16px';
|
||||
break;
|
||||
case 'top_right':
|
||||
button.style.top = '190px';
|
||||
button.style.right = '16px';
|
||||
break;
|
||||
case 'bottom_left':
|
||||
button.style.bottom = '190px';
|
||||
button.style.left = '16px';
|
||||
break;
|
||||
case 'bottom_right':
|
||||
default:
|
||||
button.style.bottom = '190px';
|
||||
button.style.right = '16px';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建侧边吸附按钮
|
||||
function createSideStickyButton() {
|
||||
widgetButton = document.createElement('div');
|
||||
widgetButton.className = 'widget-bot-button';
|
||||
widgetButton.className = 'widget-bot-button widget-bot-side-sticky';
|
||||
widgetButton.setAttribute('role', 'button');
|
||||
widgetButton.setAttribute('tabindex', '0');
|
||||
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text}窗口`);
|
||||
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text || '在线客服'}窗口`);
|
||||
widgetButton.setAttribute('data-theme', currentTheme);
|
||||
|
||||
const buttonContent = document.createElement('div');
|
||||
buttonContent.className = 'widget-bot-button-content';
|
||||
|
||||
// 添加logo(如果有)
|
||||
if (widgetInfo.btn_logo) {
|
||||
const logo = document.createElement('img');
|
||||
logo.src = widgetDomain + widgetInfo.btn_logo;
|
||||
logo.alt = 'logo';
|
||||
logo.className = 'widget-bot-logo';
|
||||
logo.onerror = () => {
|
||||
logo.style.display = 'none';
|
||||
};
|
||||
buttonContent.appendChild(logo);
|
||||
}
|
||||
// 侧边吸附显示图标和文字(btn_logo 以及 btn_text)
|
||||
const icon = document.createElement('img');
|
||||
const defaultIconSrc = widgetDomain + '/favicon.png';
|
||||
icon.src = widgetInfo.btn_logo ? (widgetDomain + widgetInfo.btn_logo) : defaultIconSrc;
|
||||
icon.alt = 'icon';
|
||||
icon.className = 'widget-bot-icon';
|
||||
icon.onerror = () => {
|
||||
// 如果当前不是 favicon.png,尝试使用 favicon.png 作为备用
|
||||
if (icon.src !== defaultIconSrc) {
|
||||
icon.src = defaultIconSrc;
|
||||
} else {
|
||||
// 如果 favicon.png 也加载失败,隐藏图标
|
||||
icon.style.display = 'none';
|
||||
}
|
||||
};
|
||||
buttonContent.appendChild(icon);
|
||||
|
||||
// 添加文字
|
||||
const textDiv = document.createElement('div');
|
||||
textDiv.className = 'widget-bot-text';
|
||||
textDiv.innerHTML = createVerticalText(widgetInfo.btn_text || '在线客服');
|
||||
textDiv.innerHTML = createTwoLineText(widgetInfo.btn_text || '在线客服');
|
||||
buttonContent.appendChild(textDiv);
|
||||
|
||||
widgetButton.appendChild(buttonContent);
|
||||
|
||||
// 应用位置 - 距离边缘16px,垂直方向190px
|
||||
const position = widgetInfo.btn_position || defaultBtnPosition;
|
||||
applyButtonPosition(widgetButton, position);
|
||||
|
||||
// 设置 border-radius 为 24px(统一圆角)
|
||||
widgetButton.style.borderRadius = '24px';
|
||||
|
||||
// 添加事件监听器
|
||||
widgetButton.addEventListener('click', showModal);
|
||||
widgetButton.addEventListener('click', handleButtonClick);
|
||||
widgetButton.addEventListener('mousedown', startDrag);
|
||||
widgetButton.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
|
|
@ -134,6 +193,69 @@
|
|||
widgetButton.addEventListener('touchend', handleTouchEnd);
|
||||
|
||||
document.body.appendChild(widgetButton);
|
||||
}
|
||||
|
||||
// 创建悬浮球按钮
|
||||
function createHoverBallButton() {
|
||||
widgetButton = document.createElement('div');
|
||||
widgetButton.className = 'widget-bot-button widget-bot-hover-ball';
|
||||
widgetButton.setAttribute('role', 'button');
|
||||
widgetButton.setAttribute('tabindex', '0');
|
||||
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text || '在线客服'}窗口`);
|
||||
widgetButton.setAttribute('data-theme', currentTheme);
|
||||
|
||||
const buttonContent = document.createElement('div');
|
||||
buttonContent.className = 'widget-bot-button-content';
|
||||
|
||||
// 悬浮球只显示图标(btn_logo)
|
||||
const icon = document.createElement('img');
|
||||
const defaultIconSrc = widgetDomain + '/favicon.png';
|
||||
icon.src = widgetInfo.btn_logo ? (widgetDomain + widgetInfo.btn_logo) : defaultIconSrc;
|
||||
icon.alt = 'icon';
|
||||
icon.className = 'widget-bot-icon widget-bot-hover-ball-icon';
|
||||
icon.onerror = () => {
|
||||
// 如果当前不是 favicon.png,尝试使用 favicon.png 作为备用
|
||||
if (icon.src !== defaultIconSrc) {
|
||||
icon.src = defaultIconSrc;
|
||||
} else {
|
||||
// 如果 favicon.png 也加载失败,隐藏图标
|
||||
icon.style.display = 'none';
|
||||
}
|
||||
};
|
||||
buttonContent.appendChild(icon);
|
||||
|
||||
widgetButton.appendChild(buttonContent);
|
||||
|
||||
// 应用位置 - 距离边缘16px,垂直方向190px
|
||||
applyButtonPosition(widgetButton, widgetInfo.btn_position || defaultBtnPosition);
|
||||
|
||||
// 添加事件监听器
|
||||
widgetButton.addEventListener('click', handleButtonClick);
|
||||
widgetButton.addEventListener('mousedown', startDrag);
|
||||
widgetButton.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// 添加触摸事件支持
|
||||
widgetButton.addEventListener('touchstart', handleTouchStart, { passive: false });
|
||||
widgetButton.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
widgetButton.addEventListener('touchend', handleTouchEnd);
|
||||
|
||||
document.body.appendChild(widgetButton);
|
||||
}
|
||||
|
||||
// 创建挂件按钮
|
||||
function createWidget() {
|
||||
// 如果已存在,先删除
|
||||
if (widgetButton) {
|
||||
widgetButton.remove();
|
||||
}
|
||||
|
||||
const btnStyle = widgetInfo.btn_style || defaultBtnStyle;
|
||||
|
||||
if (btnStyle === 'hover_ball') {
|
||||
createHoverBallButton();
|
||||
} else {
|
||||
createSideStickyButton();
|
||||
}
|
||||
|
||||
// 创建模态框
|
||||
createModal();
|
||||
|
|
@ -145,6 +267,109 @@
|
|||
}, 100);
|
||||
}
|
||||
|
||||
// 创建自定义触发按钮
|
||||
function createCustomTrigger() {
|
||||
const btnId = widgetInfo.btn_id;
|
||||
if (!btnId) {
|
||||
console.error('btn_trigger 模式需要提供 btn_id');
|
||||
return;
|
||||
}
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 50; // 最多重试 50 次(5秒)
|
||||
|
||||
// 绑定事件到元素
|
||||
function attachTrigger(element) {
|
||||
if (!element) return;
|
||||
|
||||
// 避免重复绑定
|
||||
if (element.hasAttribute('data-widget-trigger-attached')) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.setAttribute('data-widget-trigger-attached', 'true');
|
||||
customTriggerElement = element;
|
||||
|
||||
// 创建事件处理函数并保存引用
|
||||
customTriggerHandler = function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showModal();
|
||||
};
|
||||
|
||||
// 绑定点击事件
|
||||
element.addEventListener('click', customTriggerHandler);
|
||||
}
|
||||
|
||||
// 尝试查找并绑定元素
|
||||
function tryAttachTrigger() {
|
||||
const element = document.getElementById(btnId);
|
||||
if (element) {
|
||||
attachTrigger(element);
|
||||
createModal();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 立即尝试一次
|
||||
if (tryAttachTrigger()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果元素还没加载,使用多种方式监听
|
||||
function retryAttach() {
|
||||
if (tryAttachTrigger()) {
|
||||
return;
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
if (retryCount < maxRetries) {
|
||||
setTimeout(retryAttach, 100);
|
||||
} else {
|
||||
console.warn('自定义触发按钮未找到,已停止重试:', btnId);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 MutationObserver 监听 DOM 变化
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
if (tryAttachTrigger()) {
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// 开始观察 DOM 变化
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// 如果 DOM 已加载完成,立即开始重试
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
setTimeout(retryAttach, 100);
|
||||
});
|
||||
} else {
|
||||
setTimeout(retryAttach, 100);
|
||||
}
|
||||
|
||||
// 延迟断开观察器(避免无限观察)
|
||||
setTimeout(function () {
|
||||
observer.disconnect();
|
||||
}, 10000); // 10秒后断开
|
||||
}
|
||||
|
||||
// 处理按钮点击事件(区分点击和拖拽)
|
||||
function handleButtonClick(e) {
|
||||
// 如果发生了拖拽,不打开弹框
|
||||
if (hasDragged) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
showModal();
|
||||
}
|
||||
|
||||
// 键盘事件处理
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
|
|
@ -176,7 +401,8 @@
|
|||
Math.pow(touch.clientY - touchStartPos.y, 2)
|
||||
);
|
||||
|
||||
if (distance < 10) {
|
||||
// 只有在没有拖拽且移动距离很小的情况下才认为是点击
|
||||
if (!hasDragged && distance < 10) {
|
||||
// 判断为点击事件
|
||||
setTimeout(() => showModal(), 100);
|
||||
}
|
||||
|
|
@ -198,22 +424,41 @@
|
|||
widgetModal.setAttribute('aria-labelledby', 'widget-modal-title');
|
||||
widgetModal.setAttribute('data-theme', currentTheme);
|
||||
|
||||
const modalPosition = widgetInfo.modal_position || defaultModalPosition;
|
||||
if (modalPosition === 'fixed') {
|
||||
widgetModal.classList.add('widget-bot-modal-fixed');
|
||||
}
|
||||
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.className = 'widget-bot-modal-content';
|
||||
if (modalPosition === 'fixed') {
|
||||
modalContent.classList.add('widget-bot-modal-content-fixed');
|
||||
}
|
||||
|
||||
// 创建关闭按钮
|
||||
// 创建关闭按钮(透明框)
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'widget-bot-close-btn';
|
||||
closeBtn.innerHTML = '<svg t="1752218667372" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4632" id="mx_n_1752218667373" width="32" height="32"><path d="M512 939.19762963a427.19762963 427.19762963 0 1 1 0-854.39525926 427.19762963 427.19762963 0 0 1 0 854.39525926z m0-482.08605274L396.47540505 341.53519999a19.41807408 19.41807408 0 0 0-27.44421216 0l-27.44421097 27.44421217a19.41807408 19.41807408 0 0 0 0 27.44421095L457.00801422 512l-115.47281423 115.52459495a19.41807408 19.41807408 0 0 0 0 27.44421216l27.44421217 27.44421097a19.41807408 19.41807408 0 0 0 27.44421095 0L512 566.99198578l115.52459495 115.47281423a19.41807408 19.41807408 0 0 0 27.44421216 0l27.44421097-27.44421217a19.41807408 19.41807408 0 0 0 0-27.44421095l-115.47281424-115.47281423 115.47281424-115.57637689a19.41807408 19.41807408 0 0 0 0-27.44421095l-27.44421097-27.44421096a19.41807408 19.41807408 0 0 0-27.44421216 0L512 457.00801422z" p-id="4633" fill="#ffffff"></path></svg>'
|
||||
closeBtn.setAttribute('aria-label', '关闭窗口');
|
||||
closeBtn.setAttribute('type', 'button');
|
||||
closeBtn.addEventListener('click', hideModal);
|
||||
|
||||
// 创建一个内部元素来处理实际的点击事件(因为按钮设置了 pointer-events: none)
|
||||
const closeBtnArea = document.createElement('div');
|
||||
closeBtnArea.style.width = '100%';
|
||||
closeBtnArea.style.height = '100%';
|
||||
closeBtnArea.style.pointerEvents = 'auto'; // 内部元素可以接收事件
|
||||
closeBtnArea.style.cursor = 'pointer';
|
||||
closeBtnArea.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
hideModal();
|
||||
});
|
||||
closeBtn.appendChild(closeBtnArea);
|
||||
|
||||
// 创建iframe
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'widget-bot-iframe';
|
||||
iframe.src = `${widgetDomain}/widget`;
|
||||
iframe.setAttribute('title', `${widgetInfo.btn_text}服务窗口`);
|
||||
iframe.setAttribute('title', `${widgetInfo.btn_text || '在线客服'}服务窗口`);
|
||||
iframe.setAttribute('allow', 'camera; microphone; geolocation');
|
||||
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-forms allow-popups allow-presentation');
|
||||
|
||||
|
|
@ -224,6 +469,156 @@
|
|||
document.body.appendChild(widgetModal);
|
||||
}
|
||||
|
||||
// 检测是否为移动端
|
||||
function isMobile() {
|
||||
return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
// 智能定位弹框(follow模式)
|
||||
function positionModalFollow(modalContent) {
|
||||
if (!widgetButton || !modalContent) return;
|
||||
|
||||
// 移动端强制居中显示
|
||||
if (isMobile()) {
|
||||
modalContent.style.position = 'relative';
|
||||
modalContent.style.top = 'auto';
|
||||
modalContent.style.left = 'auto';
|
||||
modalContent.style.right = 'auto';
|
||||
modalContent.style.bottom = 'auto';
|
||||
modalContent.style.margin = 'auto';
|
||||
modalContent.style.width = 'calc(100% - 32px)';
|
||||
modalContent.style.height = 'auto';
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const buttonRect = widgetButton.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
const margin = 16; // 距离屏幕边缘的最小距离
|
||||
const buttonGap = 16; // 弹框和按钮之间的最小距离
|
||||
|
||||
// 先设置一个临时位置来获取弹框尺寸
|
||||
const originalPosition = modalContent.style.position;
|
||||
const originalTop = modalContent.style.top;
|
||||
const originalLeft = modalContent.style.left;
|
||||
const originalVisibility = modalContent.style.visibility;
|
||||
const originalDisplay = modalContent.style.display;
|
||||
|
||||
modalContent.style.position = 'absolute';
|
||||
modalContent.style.top = '0';
|
||||
modalContent.style.left = '0';
|
||||
modalContent.style.visibility = 'hidden';
|
||||
modalContent.style.display = 'block';
|
||||
|
||||
const modalRect = modalContent.getBoundingClientRect();
|
||||
const modalWidth = modalRect.width;
|
||||
const modalHeight = modalRect.height;
|
||||
|
||||
modalContent.style.visibility = originalVisibility || 'visible';
|
||||
modalContent.style.display = originalDisplay || 'block';
|
||||
|
||||
// 计算按钮中心点
|
||||
const buttonCenterX = buttonRect.left + buttonRect.width / 2;
|
||||
const buttonCenterY = buttonRect.top + buttonRect.height / 2;
|
||||
|
||||
// 判断按钮在屏幕的哪一侧
|
||||
const isLeftSide = buttonCenterX < windowWidth / 2;
|
||||
const isTopSide = buttonCenterY < windowHeight / 2;
|
||||
|
||||
// 智能选择弹框位置,确保完整显示
|
||||
let finalTop, finalBottom, finalLeft, finalRight;
|
||||
|
||||
if (isLeftSide) {
|
||||
// 按钮在左侧,弹框优先显示在右侧(按钮右侧)
|
||||
finalLeft = buttonRect.right + buttonGap;
|
||||
finalRight = 'auto';
|
||||
|
||||
// 如果右侧空间不够,显示在左侧(按钮左侧)
|
||||
if (finalLeft + modalWidth > windowWidth - margin) {
|
||||
finalLeft = 'auto';
|
||||
finalRight = windowWidth - buttonRect.left + buttonGap;
|
||||
// 如果左侧空间也不够,则贴左边(但保持与按钮的距离)
|
||||
if (buttonRect.left - buttonGap - modalWidth < margin) {
|
||||
finalLeft = margin;
|
||||
finalRight = 'auto';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 按钮在右侧,弹框优先显示在左侧(按钮左侧)
|
||||
finalLeft = 'auto';
|
||||
finalRight = windowWidth - buttonRect.left + buttonGap;
|
||||
|
||||
// 如果左侧空间不够,显示在右侧(按钮右侧)
|
||||
if (buttonRect.left - buttonGap - modalWidth < margin) {
|
||||
finalRight = 'auto';
|
||||
finalLeft = buttonRect.right + buttonGap;
|
||||
// 如果右侧空间也不够,则贴右边(但保持与按钮的距离)
|
||||
if (finalLeft + modalWidth > windowWidth - margin) {
|
||||
finalLeft = 'auto';
|
||||
finalRight = margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 垂直方向:优先与按钮顶部对齐
|
||||
// 弹框顶部与按钮顶部对齐
|
||||
finalTop = buttonRect.top;
|
||||
finalBottom = 'auto';
|
||||
|
||||
// 如果弹框底部超出屏幕,则向上调整,确保弹框完整显示在屏幕内
|
||||
if (finalTop + modalHeight > windowHeight - margin) {
|
||||
// 计算向上调整后的位置
|
||||
const adjustedTop = windowHeight - margin - modalHeight;
|
||||
// 如果调整后的位置仍然在按钮上方,则使用调整后的位置
|
||||
if (adjustedTop >= margin) {
|
||||
finalTop = adjustedTop;
|
||||
} else {
|
||||
// 如果调整后仍然超出,则贴顶部
|
||||
finalTop = margin;
|
||||
}
|
||||
} else if (finalTop < margin) {
|
||||
// 如果弹框顶部超出屏幕,则贴顶部
|
||||
finalTop = margin;
|
||||
}
|
||||
|
||||
// 应用最终位置
|
||||
modalContent.style.top = finalTop !== undefined ? (typeof finalTop === 'string' ? finalTop : finalTop + 'px') : 'auto';
|
||||
modalContent.style.bottom = finalBottom !== undefined ? (typeof finalBottom === 'string' ? finalBottom : finalBottom + 'px') : 'auto';
|
||||
modalContent.style.left = finalLeft !== undefined ? (typeof finalLeft === 'string' ? finalLeft : finalLeft + 'px') : 'auto';
|
||||
modalContent.style.right = finalRight !== undefined ? (typeof finalRight === 'string' ? finalRight : finalRight + 'px') : 'auto';
|
||||
|
||||
// 最终检查并修正,确保弹框完全在屏幕内
|
||||
requestAnimationFrame(() => {
|
||||
const finalModalRect = modalContent.getBoundingClientRect();
|
||||
|
||||
// 修正左边界
|
||||
if (finalModalRect.left < margin) {
|
||||
modalContent.style.left = margin + 'px';
|
||||
modalContent.style.right = 'auto';
|
||||
}
|
||||
|
||||
// 修正右边界
|
||||
if (finalModalRect.right > windowWidth - margin) {
|
||||
modalContent.style.right = margin + 'px';
|
||||
modalContent.style.left = 'auto';
|
||||
}
|
||||
|
||||
// 修正上边界
|
||||
if (finalModalRect.top < margin) {
|
||||
modalContent.style.top = margin + 'px';
|
||||
modalContent.style.bottom = 'auto';
|
||||
}
|
||||
|
||||
// 修正下边界
|
||||
if (finalModalRect.bottom > windowHeight - margin) {
|
||||
modalContent.style.bottom = margin + 'px';
|
||||
modalContent.style.top = 'auto';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 显示模态框
|
||||
function showModal() {
|
||||
if (!widgetModal) return;
|
||||
|
|
@ -231,27 +626,31 @@
|
|||
widgetModal.style.display = 'flex';
|
||||
document.body.classList.add('widget-bot-modal-open');
|
||||
|
||||
// 计算模态框位置
|
||||
requestAnimationFrame(() => {
|
||||
const buttonRect = widgetButton.getBoundingClientRect();
|
||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||
const modalPosition = widgetInfo.modal_position || defaultModalPosition;
|
||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||
|
||||
if (modalContent) {
|
||||
// 设置模态框位置:距离按钮16px,距离底部24px
|
||||
const modalBottom = 24;
|
||||
const modalRight = Math.max(16, window.innerWidth - buttonRect.left + 16);
|
||||
|
||||
modalContent.style.bottom = modalBottom + 'px';
|
||||
modalContent.style.right = modalRight + 'px';
|
||||
|
||||
// 确保模态框不会超出屏幕
|
||||
const modalRect = modalContent.getBoundingClientRect();
|
||||
if (modalRect.left < 16) {
|
||||
modalContent.style.right = '16px';
|
||||
modalContent.style.left = '16px';
|
||||
}
|
||||
}
|
||||
});
|
||||
// 移动端强制居中显示
|
||||
if (isMobile()) {
|
||||
modalContent.style.position = 'relative';
|
||||
modalContent.style.top = 'auto';
|
||||
modalContent.style.left = 'auto';
|
||||
modalContent.style.right = 'auto';
|
||||
modalContent.style.bottom = 'auto';
|
||||
modalContent.style.margin = 'auto';
|
||||
modalContent.style.width = 'calc(100% - 32px)';
|
||||
modalContent.style.height = 'auto';
|
||||
} else if (modalPosition === 'fixed') {
|
||||
// 桌面端固定模式:居中展示
|
||||
modalContent.style.position = 'relative';
|
||||
modalContent.style.top = 'auto';
|
||||
modalContent.style.left = 'auto';
|
||||
modalContent.style.right = 'auto';
|
||||
modalContent.style.bottom = 'auto';
|
||||
modalContent.style.margin = 'auto';
|
||||
} else {
|
||||
// 桌面端跟随模式:跟随按钮位置 - 智能定位,确保弹框完整显示在屏幕内
|
||||
positionModalFollow(modalContent);
|
||||
}
|
||||
|
||||
// 添加ESC键关闭功能
|
||||
document.addEventListener('keydown', handleEscKey);
|
||||
|
|
@ -287,42 +686,98 @@
|
|||
};
|
||||
|
||||
isDragging = true;
|
||||
hasDragged = false; // 重置拖拽标记
|
||||
|
||||
const rect = widgetButton.getBoundingClientRect();
|
||||
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
|
||||
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
|
||||
|
||||
dragOffset.x = clientX - rect.left;
|
||||
dragOffset.y = clientY - rect.top;
|
||||
// 记录拖拽开始位置
|
||||
dragStartPos.x = clientX;
|
||||
dragStartPos.y = clientY;
|
||||
|
||||
// 清除bottom定位,使用top定位
|
||||
widgetButton.style.bottom = 'auto';
|
||||
widgetButton.style.top = rect.top + 'px';
|
||||
// 缓存按钮尺寸,避免拖拽过程中频繁读取
|
||||
buttonSize.width = rect.width;
|
||||
buttonSize.height = rect.height;
|
||||
|
||||
// 先清除 transform,确保获取真实的位置
|
||||
widgetButton.style.transform = 'none';
|
||||
|
||||
// 重新获取位置(清除 transform 后的真实位置)
|
||||
const realRect = widgetButton.getBoundingClientRect();
|
||||
|
||||
// 记录初始位置(基于清除 transform 后的真实位置)
|
||||
initialPosition.left = realRect.left;
|
||||
initialPosition.top = realRect.top;
|
||||
|
||||
dragOffset.x = clientX - realRect.left;
|
||||
dragOffset.y = clientY - realRect.top;
|
||||
|
||||
// 确保使用 fixed 定位,使用真实位置
|
||||
widgetButton.style.position = 'fixed';
|
||||
widgetButton.style.top = realRect.top + 'px';
|
||||
widgetButton.style.left = realRect.left + 'px';
|
||||
widgetButton.style.right = 'auto';
|
||||
widgetButton.style.bottom = 'auto';
|
||||
|
||||
document.addEventListener('mousemove', drag);
|
||||
// 禁用过渡效果,提升拖拽性能
|
||||
widgetButton.style.transition = 'none';
|
||||
|
||||
// 提示浏览器优化(使用 left/top 定位)
|
||||
widgetButton.style.willChange = 'left, top';
|
||||
|
||||
document.addEventListener('mousemove', drag, { passive: false });
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
|
||||
widgetButton.classList.add('dragging');
|
||||
widgetButton.style.zIndex = '10001';
|
||||
}
|
||||
|
||||
// 拖拽中
|
||||
// 拖拽中 - 直接更新位置,实现丝滑跟随
|
||||
function drag(e) {
|
||||
if (!isDragging) return;
|
||||
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault()
|
||||
};
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
|
||||
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
|
||||
|
||||
// 检测是否发生了实际移动(超过5px才认为是拖拽)
|
||||
const moveDistance = Math.sqrt(
|
||||
Math.pow(clientX - dragStartPos.x, 2) +
|
||||
Math.pow(clientY - dragStartPos.y, 2)
|
||||
);
|
||||
if (moveDistance > 5) {
|
||||
hasDragged = true;
|
||||
}
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
const buttonWidth = buttonSize.width;
|
||||
const buttonHeight = buttonSize.height;
|
||||
|
||||
// 直接基于鼠标位置计算新位置
|
||||
// 鼠标位置减去拖拽偏移量,得到按钮左上角应该的位置
|
||||
const newLeft = clientX - dragOffset.x;
|
||||
const newTop = clientY - dragOffset.y;
|
||||
const maxTop = window.innerHeight - widgetButton.offsetHeight;
|
||||
|
||||
// 限制在屏幕范围内
|
||||
const constrainedTop = Math.max(0, Math.min(newTop, maxTop));
|
||||
// 垂直位置:限制在屏幕范围内,距离顶部和底部最小距离为 24px
|
||||
const minTop = 24;
|
||||
const maxTop = Math.max(minTop, windowHeight - buttonHeight - 24);
|
||||
const constrainedTop = Math.max(minTop, Math.min(newTop, maxTop));
|
||||
|
||||
// 水平位置:限制在屏幕范围内
|
||||
const maxLeft = windowWidth - buttonWidth;
|
||||
const constrainedLeft = Math.max(0, Math.min(newLeft, maxLeft));
|
||||
|
||||
// 直接使用 left/top 定位,实现无延迟的丝滑跟随
|
||||
// 使用 transform: none 确保不会有任何 transform 干扰
|
||||
widgetButton.style.left = constrainedLeft + 'px';
|
||||
widgetButton.style.top = constrainedTop + 'px';
|
||||
widgetButton.style.right = 'auto';
|
||||
widgetButton.style.bottom = 'auto';
|
||||
widgetButton.style.transform = 'none';
|
||||
}
|
||||
|
||||
// 停止拖拽
|
||||
|
|
@ -330,26 +785,75 @@
|
|||
if (!isDragging) return;
|
||||
|
||||
isDragging = false;
|
||||
|
||||
// 取消待执行的动画帧
|
||||
if (dragAnimationFrame) {
|
||||
cancelAnimationFrame(dragAnimationFrame);
|
||||
dragAnimationFrame = null;
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', drag);
|
||||
document.removeEventListener('mouseup', stopDrag);
|
||||
|
||||
widgetButton.classList.remove('dragging');
|
||||
widgetButton.style.zIndex = '9999';
|
||||
|
||||
// 吸附到右侧,恢复bottom定位
|
||||
// 恢复过渡效果
|
||||
widgetButton.style.transition = '';
|
||||
widgetButton.style.willChange = '';
|
||||
|
||||
// 根据按钮类型和当前位置进行最终定位
|
||||
requestAnimationFrame(() => {
|
||||
const currentTop = parseInt(widgetButton.style.top);
|
||||
const buttonRect = widgetButton.getBoundingClientRect();
|
||||
const currentLeft = buttonRect.left;
|
||||
const currentTop = buttonRect.top;
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
const buttonHeight = widgetButton.offsetHeight;
|
||||
const buttonWidth = buttonSize.width;
|
||||
const buttonHeight = buttonSize.height;
|
||||
|
||||
// 计算距离底部的位置
|
||||
const bottomPosition = windowHeight - currentTop - buttonHeight;
|
||||
// 两种模式使用相同的停止拖拽逻辑:只能左右侧边缘吸附
|
||||
// 根据按钮实际位置判断左右,保持当前位置
|
||||
const screenCenterX = windowWidth / 2;
|
||||
const buttonCenterX = currentLeft + buttonWidth / 2;
|
||||
const isLeftSide = buttonCenterX < screenCenterX;
|
||||
const sideDistance = 16; // 距离边缘的距离
|
||||
|
||||
// 恢复right和bottom定位,清除top
|
||||
widgetButton.style.right = '0';
|
||||
widgetButton.style.bottom = Math.max(20, bottomPosition) + 'px';
|
||||
widgetButton.style.top = 'auto';
|
||||
widgetButton.style.left = 'auto';
|
||||
// 垂直位置:保持在当前位置,限制在屏幕范围内,距离顶部和底部最小距离为 24px
|
||||
const minTop = 24;
|
||||
const maxTop = Math.max(minTop, windowHeight - buttonHeight - 24);
|
||||
const finalTop = Math.max(minTop, Math.min(currentTop, maxTop));
|
||||
let finalLeft;
|
||||
|
||||
// 水平位置:距离左右边16px
|
||||
if (isLeftSide) {
|
||||
finalLeft = sideDistance;
|
||||
widgetButton.style.left = sideDistance + 'px';
|
||||
widgetButton.style.right = 'auto';
|
||||
} else {
|
||||
finalLeft = windowWidth - sideDistance - buttonWidth;
|
||||
widgetButton.style.right = sideDistance + 'px';
|
||||
widgetButton.style.left = 'auto';
|
||||
}
|
||||
|
||||
widgetButton.style.top = finalTop + 'px';
|
||||
widgetButton.style.bottom = 'auto';
|
||||
|
||||
// 清除 transform,使用 left/top 定位
|
||||
widgetButton.style.transform = 'none';
|
||||
|
||||
// 更新 border-radius(现在都是24px圆角)
|
||||
widgetButton.style.borderRadius = '24px';
|
||||
|
||||
// 更新初始位置,为下次拖拽做准备
|
||||
if (finalLeft !== undefined && finalTop !== undefined) {
|
||||
initialPosition.left = finalLeft;
|
||||
initialPosition.top = finalTop;
|
||||
} else {
|
||||
// 如果未定义,使用当前实际位置
|
||||
initialPosition.left = buttonRect.left;
|
||||
initialPosition.top = buttonRect.top;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -390,19 +894,30 @@
|
|||
// 窗口大小改变时重新定位
|
||||
window.addEventListener('resize', function () {
|
||||
if (widgetModal && widgetModal.style.display === 'flex') {
|
||||
// 重新计算模态框位置
|
||||
setTimeout(() => {
|
||||
const buttonRect = widgetButton.getBoundingClientRect();
|
||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||
if (!modalContent) return;
|
||||
|
||||
if (modalContent) {
|
||||
const modalBottom = 24;
|
||||
const modalRight = Math.max(16, window.innerWidth - buttonRect.left + 16);
|
||||
// 移动端强制居中显示
|
||||
if (isMobile()) {
|
||||
modalContent.style.position = 'relative';
|
||||
modalContent.style.top = 'auto';
|
||||
modalContent.style.left = 'auto';
|
||||
modalContent.style.right = 'auto';
|
||||
modalContent.style.bottom = 'auto';
|
||||
modalContent.style.margin = 'auto';
|
||||
modalContent.style.width = 'calc(100% - 32px)';
|
||||
modalContent.style.height = 'auto';
|
||||
return;
|
||||
}
|
||||
|
||||
modalContent.style.bottom = modalBottom + 'px';
|
||||
modalContent.style.right = modalRight + 'px';
|
||||
}
|
||||
}, 100);
|
||||
const modalPosition = widgetInfo?.modal_position || defaultModalPosition;
|
||||
if (modalPosition === 'fixed') {
|
||||
// 固定居中模式不需要重新定位
|
||||
return;
|
||||
}
|
||||
|
||||
// 重新计算模态框位置(使用智能定位)
|
||||
positionModalFollow(modalContent);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -423,8 +938,13 @@
|
|||
if (widgetModal) {
|
||||
widgetModal.remove();
|
||||
}
|
||||
if (customTriggerElement && customTriggerHandler) {
|
||||
customTriggerElement.removeEventListener('click', customTriggerHandler);
|
||||
customTriggerElement.removeAttribute('data-widget-trigger-attached');
|
||||
}
|
||||
});
|
||||
|
||||
// 启动
|
||||
init();
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { ThemeStoreProvider } from '@/provider/themeStore';
|
|||
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
||||
import { getShareProV1AuthInfo } from '@/request/pro/ShareAuth';
|
||||
import { Box } from '@mui/material';
|
||||
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
|
||||
import { AppRouterCacheProvider } from '@mui/material-nextjs/v16-appRouter';
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import localFont from 'next/font/local';
|
||||
import { headers, cookies } from 'next/headers';
|
||||
|
|
@ -92,7 +92,7 @@ const Layout = async ({
|
|||
return (
|
||||
<html lang='en'>
|
||||
<body
|
||||
className={`${gilory.variable} ${themeMode === 'dark' ? 'dark' : ''}`}
|
||||
className={`${gilory.variable} ${themeMode === 'dark' ? 'dark' : 'light'}`}
|
||||
>
|
||||
<AppRouterCacheProvider>
|
||||
<ThemeStoreProvider themeMode={themeMode}>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
--color-primary-main: #6e73fe;
|
||||
|
||||
/* 代码块颜色 */
|
||||
--code-bg: #ffffff;
|
||||
--code-bg: rgba(0, 0, 0, 0.03);
|
||||
--code-color: #21222d;
|
||||
--inline-code-bg: #fff5f5;
|
||||
--inline-code-color: #ff502c;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import StoreProvider from '@/provider';
|
||||
import { darkThemeWidget, lightThemeWidget } from '@/theme';
|
||||
import { getShareV1AppWidgetInfo } from '@/request/ShareApp';
|
||||
import { ThemeProvider } from '@ctzhian/ui';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const Layout = async ({
|
||||
|
|
@ -12,18 +9,7 @@ const Layout = async ({
|
|||
}>) => {
|
||||
const widgetDetail: any = await getShareV1AppWidgetInfo();
|
||||
|
||||
const themeMode =
|
||||
widgetDetail?.settings?.widget_bot_settings?.theme_mode || 'light';
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
theme={themeMode === 'dark' ? darkThemeWidget : lightThemeWidget}
|
||||
>
|
||||
<StoreProvider widget={widgetDetail} themeMode={themeMode || 'light'}>
|
||||
{children}
|
||||
</StoreProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
return <StoreProvider widget={widgetDetail}>{children}</StoreProvider>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,3 @@
|
|||
import Widget from '@/views/widget';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<Widget />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
export default Widget;
|
||||
|
|
|
|||
|
|
@ -109,10 +109,19 @@ export type WidgetInfo = {
|
|||
search_placeholder: string;
|
||||
recommend_questions: string[];
|
||||
widget_bot_settings: {
|
||||
btn_logo: string;
|
||||
btn_text: string;
|
||||
is_open: boolean;
|
||||
theme_mode: 'light' | 'dark';
|
||||
btn_logo?: string;
|
||||
btn_text?: string;
|
||||
btn_style?: string;
|
||||
btn_id?: string;
|
||||
btn_position?: string;
|
||||
modal_position?: string;
|
||||
is_open?: boolean;
|
||||
recommend_node_ids?: string[];
|
||||
recommend_questions?: string[];
|
||||
theme_mode?: string;
|
||||
search_mode?: string;
|
||||
placeholder?: string;
|
||||
disclaimer?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ export interface ConversationItem {
|
|||
message_id: string;
|
||||
source: 'history' | 'chat';
|
||||
chunk_result: ChunkResultItem[];
|
||||
result_expend: boolean;
|
||||
thinking_expend: boolean;
|
||||
thinking_content: string;
|
||||
id: string;
|
||||
}
|
||||
|
|
@ -382,6 +384,8 @@ const AiQaContent: React.FC<{
|
|||
const solution = await cap.solve();
|
||||
token = solution.token;
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setThinking(4);
|
||||
message.error('验证失败');
|
||||
console.log(error, 'error---------');
|
||||
return;
|
||||
|
|
@ -465,6 +469,8 @@ const AiQaContent: React.FC<{
|
|||
if (lastConversation) {
|
||||
lastConversation.a = answerContent;
|
||||
lastConversation.thinking_content = thinkingContent;
|
||||
lastConversation.result_expend = false;
|
||||
lastConversation.thinking_expend = false;
|
||||
}
|
||||
return newConversation;
|
||||
});
|
||||
|
|
@ -513,6 +519,8 @@ const AiQaContent: React.FC<{
|
|||
source: 'chat',
|
||||
chunk_result: [],
|
||||
thinking_content: '',
|
||||
result_expend: true,
|
||||
thinking_expend: true,
|
||||
id: uuidv4(),
|
||||
});
|
||||
messageIdRef.current = '';
|
||||
|
|
@ -631,6 +639,8 @@ const AiQaContent: React.FC<{
|
|||
source: 'history',
|
||||
chunk_result: [],
|
||||
thinking_content: '',
|
||||
result_expend: true,
|
||||
thinking_expend: true,
|
||||
id: uuidv4(),
|
||||
});
|
||||
}
|
||||
|
|
@ -667,6 +677,8 @@ const AiQaContent: React.FC<{
|
|||
chunk_result: [],
|
||||
thinking_content: '',
|
||||
id: uuidv4(),
|
||||
result_expend: true,
|
||||
thinking_expend: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -791,7 +803,16 @@ const AiQaContent: React.FC<{
|
|||
<StyledAiBubble>
|
||||
{/* 搜索结果 */}
|
||||
{item.chunk_result.length > 0 && (
|
||||
<StyledChunkAccordion defaultExpanded>
|
||||
<StyledChunkAccordion
|
||||
expanded={item.result_expend}
|
||||
onChange={(event, expanded) => {
|
||||
setConversation(prev => {
|
||||
const newConversation = [...prev];
|
||||
newConversation[index].result_expend = expanded;
|
||||
return newConversation;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<StyledChunkAccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
|
|
@ -837,7 +858,16 @@ const AiQaContent: React.FC<{
|
|||
|
||||
{/* 思考过程 */}
|
||||
{!!item.thinking_content && (
|
||||
<StyledThinkingAccordion defaultExpanded>
|
||||
<StyledThinkingAccordion
|
||||
expanded={item.thinking_expend}
|
||||
onChange={(event, expanded) => {
|
||||
setConversation(prev => {
|
||||
const newConversation = [...prev];
|
||||
newConversation[index].thinking_expend = expanded;
|
||||
return newConversation;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<StyledThinkingAccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
|
|
@ -929,6 +959,9 @@ const AiQaContent: React.FC<{
|
|||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<Box>
|
||||
{kbDetail?.settings?.disclaimer_settings?.content}
|
||||
</Box>
|
||||
</StyledActionStack>
|
||||
)}
|
||||
</StyledAiBubble>
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ export const StyledUserBubble = styled(Box)(({ theme }) => ({
|
|||
|
||||
export const StyledAiBubble = styled(Box)(({ theme }) => ({
|
||||
alignSelf: 'flex-start',
|
||||
maxWidth: '85%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: theme.spacing(3),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -247,7 +247,9 @@ const QaModal: React.FC<QaModalProps> = () => {
|
|||
<Box
|
||||
sx={{
|
||||
px: 3,
|
||||
pt: kbDetail?.settings?.disclaimer_settings?.content ? 2 : 0,
|
||||
pt: kbDetail?.settings?.web_app_custom_style?.show_brand_info
|
||||
? 2
|
||||
: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
|
@ -263,7 +265,10 @@ const QaModal: React.FC<QaModalProps> = () => {
|
|||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box>{kbDetail?.settings?.disclaimer_settings?.content}</Box>
|
||||
<Box>
|
||||
{kbDetail?.settings?.web_app_custom_style?.show_brand_info &&
|
||||
'本网站由 PandaWiki 提供技术支持'}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import Logo from '@/assets/images/logo.png';
|
||||
import { Box } from '@mui/material';
|
||||
import { Stack, Box, IconButton, alpha, Tooltip } from '@mui/material';
|
||||
import { postShareProV1AuthLogout } from '@/request/pro/ShareAuth';
|
||||
import { IconDengchu } from '@panda-wiki/icons';
|
||||
import { useStore } from '@/provider';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import { Modal } from '@ctzhian/ui';
|
||||
import {
|
||||
Header as CustomHeader,
|
||||
WelcomeHeader as WelcomeHeaderComponent,
|
||||
|
|
@ -16,8 +20,58 @@ interface HeaderProps {
|
|||
isWelcomePage?: boolean;
|
||||
}
|
||||
|
||||
const LogoutButton = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleLogout = () => {
|
||||
return postShareProV1AuthLogout().then(() => {
|
||||
// 使用当前页面的协议(http 或 https)
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
window.location.href = `${protocol}//${host}/auth/login`;
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
<ErrorIcon sx={{ fontSize: 24, color: 'warning.main' }} />
|
||||
<Box sx={{ mt: '2px' }}>提示</Box>
|
||||
</Stack>
|
||||
}
|
||||
open={open}
|
||||
okText='确定'
|
||||
cancelText='取消'
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={handleLogout}
|
||||
closable={false}
|
||||
>
|
||||
<Box sx={{ pl: 4 }}>确定要退出登录吗?</Box>
|
||||
</Modal>
|
||||
<Tooltip title='退出登录' arrow>
|
||||
<IconButton size='small' onClick={() => setOpen(true)}>
|
||||
<IconDengchu
|
||||
sx={theme => ({
|
||||
cursor: 'pointer',
|
||||
color: alpha(theme.palette.text.primary, 0.65),
|
||||
fontSize: 24,
|
||||
'&:hover': { color: theme.palette.primary.main },
|
||||
})}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = ({ isDocPage = false, isWelcomePage = false }: HeaderProps) => {
|
||||
const { mobile = false, kbDetail, catalogWidth, setQaModalOpen } = useStore();
|
||||
const {
|
||||
mobile = false,
|
||||
kbDetail,
|
||||
catalogWidth,
|
||||
setQaModalOpen,
|
||||
authInfo,
|
||||
} = useStore();
|
||||
const pathname = usePathname();
|
||||
const docWidth = useMemo(() => {
|
||||
if (isWelcomePage) return 'full';
|
||||
|
|
@ -55,16 +109,23 @@ const Header = ({ isDocPage = false, isWelcomePage = false }: HeaderProps) => {
|
|||
onSearch={handleSearch}
|
||||
onQaClick={() => setQaModalOpen?.(true)}
|
||||
>
|
||||
<Box sx={{ ml: 2 }}>
|
||||
<Stack sx={{ ml: 2 }} direction='row' alignItems='center' gap={1}>
|
||||
<ThemeSwitch />
|
||||
</Box>
|
||||
{!!authInfo && <LogoutButton />}
|
||||
</Stack>
|
||||
<QaModal />
|
||||
</CustomHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export const WelcomeHeader = () => {
|
||||
const { mobile = false, kbDetail, catalogWidth, setQaModalOpen } = useStore();
|
||||
const {
|
||||
mobile = false,
|
||||
kbDetail,
|
||||
catalogWidth,
|
||||
setQaModalOpen,
|
||||
authInfo,
|
||||
} = useStore();
|
||||
const handleSearch = (value?: string, type: 'chat' | 'search' = 'chat') => {
|
||||
if (value?.trim()) {
|
||||
if (type === 'chat') {
|
||||
|
|
@ -91,6 +152,7 @@ export const WelcomeHeader = () => {
|
|||
onSearch={handleSearch}
|
||||
onQaClick={() => setQaModalOpen?.(true)}
|
||||
>
|
||||
<Box sx={{ ml: 2 }}>{!!authInfo && <LogoutButton />}</Box>
|
||||
<QaModal />
|
||||
</WelcomeHeaderComponent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import React, { useState, useEffect } from 'react';
|
|||
import { createRoot } from 'react-dom/client';
|
||||
import { styled, SvgIcon, SvgIconProps } from '@mui/material';
|
||||
|
||||
// ==================== 图片数据缓存 ====================
|
||||
// 全局图片 blob URL 缓存,避免重复请求 OSS
|
||||
const imageBlobCache = new Map<string, string>();
|
||||
|
||||
// ==================== 图片数据缓存工具函数 ====================
|
||||
// 下载图片并转换为 blob URL
|
||||
const fetchImageAsBlob = async (src: string): Promise<string> => {
|
||||
const fetchImageAsBlob = async (
|
||||
src: string,
|
||||
imageBlobCache: Map<string, string>,
|
||||
): Promise<string> => {
|
||||
// 检查缓存
|
||||
if (imageBlobCache.has(src)) {
|
||||
return imageBlobCache.get(src)!;
|
||||
|
|
@ -39,12 +39,8 @@ const fetchImageAsBlob = async (src: string): Promise<string> => {
|
|||
}
|
||||
};
|
||||
|
||||
// 导出获取图片 blob URL 的函数
|
||||
export const getImageBlobUrl = (src: string): string | null => {
|
||||
return imageBlobCache.get(src) || null;
|
||||
};
|
||||
|
||||
export const clearImageBlobCache = () => {
|
||||
// 清理图片 blob 缓存
|
||||
export const clearImageBlobCache = (imageBlobCache: Map<string, string>) => {
|
||||
imageBlobCache.forEach(url => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
|
@ -54,7 +50,7 @@ export const clearImageBlobCache = () => {
|
|||
const StyledErrorContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing(2),
|
||||
padding: theme.spacing(1, 6),
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '8px',
|
||||
|
|
@ -71,7 +67,7 @@ const StyledErrorContainer = styled('div')(({ theme }) => ({
|
|||
|
||||
const StyledErrorText = styled('div')(() => ({
|
||||
fontSize: '12px',
|
||||
marginBottom: 16,
|
||||
marginBottom: 10,
|
||||
}));
|
||||
|
||||
export const ImageErrorIcon = (props: SvgIconProps) => {
|
||||
|
|
@ -102,7 +98,7 @@ export const ImageErrorIcon = (props: SvgIconProps) => {
|
|||
const ImageErrorDisplay: React.FC = () => (
|
||||
<StyledErrorContainer>
|
||||
<ImageErrorIcon
|
||||
sx={{ color: 'var(--mui-palette-text-tertiary)', fontSize: 160 }}
|
||||
sx={{ color: 'var(--mui-palette-text-tertiary)', fontSize: 140 }}
|
||||
/>
|
||||
<StyledErrorText>图片加载失败</StyledErrorText>
|
||||
</StyledErrorContainer>
|
||||
|
|
@ -116,7 +112,7 @@ interface ImageComponentProps {
|
|||
imageIndex: number;
|
||||
onLoad: (index: number, html: string) => void;
|
||||
onError: (index: number, html: string) => void;
|
||||
onImageClick: (src: string) => void;
|
||||
imageBlobCache: Map<string, string>;
|
||||
}
|
||||
|
||||
// ==================== 图片组件 ====================
|
||||
|
|
@ -127,7 +123,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
|||
imageIndex,
|
||||
onLoad,
|
||||
onError,
|
||||
onImageClick,
|
||||
imageBlobCache,
|
||||
}) => {
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>(
|
||||
'loading',
|
||||
|
|
@ -149,7 +145,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
|||
// 获取图片 blob URL
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
fetchImageAsBlob(src)
|
||||
fetchImageAsBlob(src, imageBlobCache)
|
||||
.then(url => {
|
||||
if (mounted) {
|
||||
setBlobUrl(url);
|
||||
|
|
@ -166,7 +162,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
|||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [src]);
|
||||
}, [src, imageBlobCache]);
|
||||
|
||||
// 解析自定义样式
|
||||
const parseStyleString = (styleStr: string) => {
|
||||
|
|
@ -238,7 +234,8 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
|||
referrerPolicy='no-referrer'
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
onClick={() => onImageClick(src)} // 传递原始 src 用于预览
|
||||
data-original-src={src}
|
||||
className='markdown-image'
|
||||
{...getOtherProps()}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -264,12 +261,13 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
|||
export interface ImageRendererOptions {
|
||||
onImageLoad: (index: number, html: string) => void;
|
||||
onImageError: (index: number, html: string) => void;
|
||||
onImageClick: (src: string) => void;
|
||||
imageRenderCache: Map<number, string>;
|
||||
imageBlobCache: Map<string, string>;
|
||||
}
|
||||
|
||||
export const createImageRenderer = (options: ImageRendererOptions) => {
|
||||
const { onImageLoad, onImageError, imageRenderCache, onImageClick } = options;
|
||||
const { onImageLoad, onImageError, imageRenderCache, imageBlobCache } =
|
||||
options;
|
||||
return (
|
||||
src: string,
|
||||
alt: string,
|
||||
|
|
@ -279,29 +277,6 @@ export const createImageRenderer = (options: ImageRendererOptions) => {
|
|||
// 检查缓存
|
||||
const cached = imageRenderCache.get(imageIndex);
|
||||
if (cached) {
|
||||
// 下一帧对已缓存的DOM绑定原生点击事件,避免事件丢失且不引起重渲染
|
||||
requestAnimationFrame(() => {
|
||||
const container = document.querySelector(
|
||||
`.image-container-${imageIndex}`,
|
||||
) as HTMLElement | null;
|
||||
if (!container) return;
|
||||
const img = container.querySelector('img') as HTMLImageElement | null;
|
||||
if (!img) return;
|
||||
const alreadyBound = (img as HTMLElement).getAttribute(
|
||||
'data-click-bound',
|
||||
);
|
||||
if (!alreadyBound) {
|
||||
(img as HTMLElement).setAttribute('data-click-bound', '1');
|
||||
img.style.cursor = img.style.cursor || 'pointer';
|
||||
img.addEventListener('click', () => {
|
||||
try {
|
||||
onImageClick(img.src);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return cached;
|
||||
}
|
||||
|
||||
|
|
@ -323,7 +298,7 @@ export const createImageRenderer = (options: ImageRendererOptions) => {
|
|||
imageIndex={imageIndex}
|
||||
onLoad={onImageLoad}
|
||||
onError={onImageError}
|
||||
onImageClick={onImageClick}
|
||||
imageBlobCache={imageBlobCache}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -15,11 +15,7 @@ import React, {
|
|||
useState,
|
||||
} from 'react';
|
||||
import { useSmartScroll } from '@/hooks';
|
||||
import {
|
||||
clearImageBlobCache,
|
||||
createImageRenderer,
|
||||
getImageBlobUrl,
|
||||
} from './imageRenderer';
|
||||
import { clearImageBlobCache, createImageRenderer } from './imageRenderer';
|
||||
import { incrementalRender } from './incrementalRenderer';
|
||||
import { createMermaidRenderer } from './mermaidRenderer';
|
||||
import {
|
||||
|
|
@ -88,7 +84,8 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
const lastContentRef = useRef<string>('');
|
||||
const mdRef = useRef<MarkdownIt | null>(null);
|
||||
const mermaidSuccessIdRef = useRef<Map<number, string>>(new Map());
|
||||
const imageRenderCacheRef = useRef<Map<number, string>>(new Map()); // 图片渲染缓存
|
||||
const imageRenderCacheRef = useRef<Map<number, string>>(new Map()); // 图片渲染缓存(HTML)
|
||||
const imageBlobCacheRef = useRef<Map<string, string>>(new Map()); // 图片 blob URL 缓存
|
||||
|
||||
// 使用智能滚动 hook
|
||||
const { scrollToBottom } = useSmartScroll({
|
||||
|
|
@ -125,13 +122,8 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
createImageRenderer({
|
||||
onImageLoad: handleImageLoad,
|
||||
onImageError: handleImageError,
|
||||
onImageClick: (src: string) => {
|
||||
// 尝试获取缓存的 blob URL,如果不存在则使用原始 src
|
||||
const blobUrl = getImageBlobUrl(src);
|
||||
setPreviewImgBlobUrl(blobUrl || src);
|
||||
setPreviewOpen(true);
|
||||
},
|
||||
imageRenderCache: imageRenderCacheRef.current,
|
||||
imageBlobCache: imageBlobCacheRef.current,
|
||||
}),
|
||||
[handleImageLoad, handleImageError],
|
||||
);
|
||||
|
|
@ -158,6 +150,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
const originalFenceRender = md.renderer.rules.fence;
|
||||
// 自定义图片渲染
|
||||
let imageCount = 0;
|
||||
let htmlImageCount = 0; // HTML 标签图片计数
|
||||
let mermaidCount = 0;
|
||||
md.renderer.rules.image = (tokens, idx) => {
|
||||
imageCount++;
|
||||
|
|
@ -240,6 +233,38 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
);
|
||||
};
|
||||
|
||||
// 解析 HTML img 标签并提取属性
|
||||
const parseImgTag = (
|
||||
html: string,
|
||||
): {
|
||||
src: string;
|
||||
alt: string;
|
||||
attrs: [string, string][];
|
||||
} | null => {
|
||||
// 匹配 <img> 标签(支持自闭合和普通标签)
|
||||
const imgMatch = html.match(/<img\s+([^>]*?)\/?>/i);
|
||||
if (!imgMatch) return null;
|
||||
|
||||
const attrsString = imgMatch[1];
|
||||
const attrs: [string, string][] = [];
|
||||
let src = '';
|
||||
let alt = '';
|
||||
|
||||
// 解析属性:匹配 name="value" 或 name='value' 或 name=value
|
||||
const attrRegex =
|
||||
/(\w+)(?:=["']([^"']*)["']|=(?:["'])?([^\s>]+)(?:["'])?)?/g;
|
||||
let attrMatch;
|
||||
while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
|
||||
const name = attrMatch[1].toLowerCase();
|
||||
const value = attrMatch[2] || attrMatch[3] || '';
|
||||
attrs.push([name, value]);
|
||||
if (name === 'src') src = value;
|
||||
if (name === 'alt') alt = value;
|
||||
}
|
||||
|
||||
return { src, alt, attrs };
|
||||
};
|
||||
|
||||
md.renderer.rules.html_block = (
|
||||
tokens,
|
||||
idx,
|
||||
|
|
@ -278,6 +303,21 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
if (content.includes('<error>')) return '<span class="chat-error">';
|
||||
if (content.includes('</error>')) return '</span>';
|
||||
|
||||
// 处理 img 标签
|
||||
if (content.includes('<img')) {
|
||||
const imgData = parseImgTag(content);
|
||||
if (imgData && imgData.src) {
|
||||
const imageIndex = imageCount + htmlImageCount;
|
||||
htmlImageCount++;
|
||||
return renderImage(
|
||||
imgData.src,
|
||||
imgData.alt,
|
||||
imgData.attrs,
|
||||
imageIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔒 安全检查:不在白名单的标签,转义输出
|
||||
if (!isAllowedTag(content)) {
|
||||
return md.utils.escapeHtml(content);
|
||||
|
|
@ -301,6 +341,21 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
if (content.includes('<error>')) return '<span class="chat-error">';
|
||||
if (content.includes('</error>')) return '</span>';
|
||||
|
||||
// 处理 img 标签
|
||||
if (content.includes('<img')) {
|
||||
const imgData = parseImgTag(content);
|
||||
if (imgData && imgData.src) {
|
||||
const imageIndex = imageCount + htmlImageCount;
|
||||
htmlImageCount++;
|
||||
return renderImage(
|
||||
imgData.src,
|
||||
imgData.alt,
|
||||
imgData.attrs,
|
||||
imageIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔒 安全检查:不在白名单的标签,转义输出
|
||||
if (!isAllowedTag(content)) {
|
||||
return md.utils.escapeHtml(content);
|
||||
|
|
@ -352,7 +407,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
}
|
||||
}, [content, customizeRenderer, scrollToBottom]);
|
||||
|
||||
// 添加代码块点击复制功能
|
||||
// 添加代码块点击复制和图片点击预览功能(事件代理)
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
|
@ -360,6 +415,21 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
const handleClick = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// 检查是否点击了图片
|
||||
const imgElement = target.closest(
|
||||
'img.markdown-image',
|
||||
) as HTMLImageElement;
|
||||
if (imgElement) {
|
||||
const originalSrc = imgElement.getAttribute('data-original-src');
|
||||
if (originalSrc) {
|
||||
// 尝试获取缓存的 blob URL,如果不存在则使用原始 src
|
||||
const blobUrl = imageBlobCacheRef.current.get(originalSrc);
|
||||
setPreviewImgBlobUrl(blobUrl || originalSrc);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否点击了代码块
|
||||
const preElement = target.closest('pre.hljs');
|
||||
if (preElement) {
|
||||
|
|
@ -368,6 +438,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
const code = codeElement.textContent || '';
|
||||
copyText(code.replace(/\n$/, ''));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否点击了行内代码
|
||||
|
|
@ -380,7 +451,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
container.addEventListener('click', handleClick);
|
||||
|
||||
return () => {
|
||||
clearImageBlobCache();
|
||||
clearImageBlobCache(imageBlobCacheRef.current);
|
||||
container.removeEventListener('click', handleClick);
|
||||
};
|
||||
}, []);
|
||||
|
|
@ -406,6 +477,9 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
},
|
||||
'.markdown-image': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'.image-error': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getShareV1AppWidgetInfo } from './request/ShareApp';
|
||||
import { middleware as homeMiddleware } from './middleware/home';
|
||||
|
||||
const proxyShare = async (request: NextRequest) => {
|
||||
// 转发到 process.env.TARGET
|
||||
const kb_id = request.headers.get('x-kb-id') || process.env.DEV_KB_ID || '';
|
||||
|
||||
const targetOrigin = process.env.TARGET!;
|
||||
const targetUrl = new URL(
|
||||
request.nextUrl.pathname + request.nextUrl.search,
|
||||
targetOrigin,
|
||||
);
|
||||
// 构造 fetch 选项
|
||||
const fetchHeaders = new Headers(request.headers);
|
||||
fetchHeaders.set('x-kb-id', kb_id);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: request.method,
|
||||
headers: fetchHeaders,
|
||||
body: ['GET', 'HEAD'].includes(request.method) ? undefined : request.body,
|
||||
redirect: 'manual',
|
||||
};
|
||||
const proxyRes = await fetch(targetUrl.toString(), fetchOptions);
|
||||
const nextRes = new NextResponse(proxyRes.body, {
|
||||
status: proxyRes.status,
|
||||
headers: proxyRes.headers,
|
||||
statusText: proxyRes.statusText,
|
||||
});
|
||||
return nextRes;
|
||||
};
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const url = request.nextUrl.clone();
|
||||
const pathname = url.pathname;
|
||||
if (pathname.startsWith('/widget')) {
|
||||
const widgetInfo: any = await getShareV1AppWidgetInfo();
|
||||
if (widgetInfo) {
|
||||
if (!widgetInfo?.settings?.widget_bot_settings?.is_open) {
|
||||
return NextResponse.rewrite(new URL('/not-fount', request.url));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [key, value] of request.headers.entries()) {
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
let sessionId = request.cookies.get('x-pw-session-id')?.value || '';
|
||||
let needSetSessionId = false;
|
||||
|
||||
if (!sessionId) {
|
||||
sessionId = uuidv4();
|
||||
needSetSessionId = true;
|
||||
}
|
||||
|
||||
let response: NextResponse;
|
||||
|
||||
if (pathname.startsWith('/share/')) {
|
||||
response = await proxyShare(request);
|
||||
} else {
|
||||
response = await homeMiddleware(request, headers, sessionId);
|
||||
}
|
||||
|
||||
if (needSetSessionId) {
|
||||
response.cookies.set('x-pw-session-id', sessionId, {
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 年
|
||||
});
|
||||
}
|
||||
if (!pathname.startsWith('/share')) {
|
||||
response.headers.set('x-current-path', pathname);
|
||||
response.headers.set('x-current-search', url.search);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/',
|
||||
'/home',
|
||||
'/share/:path*',
|
||||
'/chat/:path*',
|
||||
'/widget',
|
||||
'/welcome',
|
||||
'/auth/login',
|
||||
'/node/:path*',
|
||||
'/node',
|
||||
// '/client/:path*',
|
||||
],
|
||||
};
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import { parsePathname } from '@/utils';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { postShareV1StatPage } from '@/request/ShareStat';
|
||||
import { getShareV1NodeList } from '@/request/ShareNode';
|
||||
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
||||
import { filterEmptyFolders, convertToTree } from '@/utils/drag';
|
||||
import { deepSearchFirstNode } from '@/utils';
|
||||
|
||||
const StatPage = {
|
||||
welcome: 1,
|
||||
node: 2,
|
||||
chat: 3,
|
||||
auth: 4,
|
||||
} as const;
|
||||
|
||||
const getFirstNode = async () => {
|
||||
const nodeListResult: any = await getShareV1NodeList();
|
||||
const tree = filterEmptyFolders(convertToTree(nodeListResult || []));
|
||||
return deepSearchFirstNode(tree);
|
||||
};
|
||||
|
||||
const getHomePath = async () => {
|
||||
const info = await getShareV1AppWebInfo();
|
||||
return info?.settings?.home_page_setting;
|
||||
};
|
||||
|
||||
export async function middleware(
|
||||
request: NextRequest,
|
||||
headers: Record<string, string>,
|
||||
session: string,
|
||||
) {
|
||||
const url = request.nextUrl.clone();
|
||||
const { page, id } = parsePathname(url.pathname);
|
||||
try {
|
||||
// 获取节点列表
|
||||
if (url.pathname === '/') {
|
||||
const homePath = await getHomePath();
|
||||
if (homePath === 'custom') {
|
||||
return NextResponse.rewrite(new URL('/home', request.url));
|
||||
} else {
|
||||
const [firstNode] = await Promise.all([getFirstNode(), getHomePath()]);
|
||||
if (firstNode) {
|
||||
return NextResponse.rewrite(
|
||||
new URL(`/node/${firstNode.id}`, request.url),
|
||||
);
|
||||
}
|
||||
return NextResponse.rewrite(new URL('/node', request.url));
|
||||
}
|
||||
}
|
||||
|
||||
// 页面上报
|
||||
const pages = Object.keys(StatPage);
|
||||
if (pages.includes(page) || pages.includes(id)) {
|
||||
postShareV1StatPage(
|
||||
{
|
||||
scene: StatPage[page as keyof typeof StatPage],
|
||||
node_id: id || '',
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'x-pw-session-id': session,
|
||||
...headers,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
} catch (error) {
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'message' in error &&
|
||||
error.message === 'NEXT_REDIRECT'
|
||||
) {
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
`/auth/login?redirect=${encodeURIComponent(url.pathname + url.search)}`,
|
||||
request.url,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
|
@ -35,6 +35,10 @@ export const ThemeStoreProvider = ({
|
|||
useEffect(() => {
|
||||
Cookies.set('theme_mode', themeMode, { expires: 365 * 10 });
|
||||
}, [themeMode]);
|
||||
|
||||
console.log('themeMode-------', themeMode);
|
||||
console.log('themeMode-------', theme);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ themeMode, setThemeMode }}>
|
||||
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getShareV1AppWidgetInfo } from './request/ShareApp';
|
||||
|
||||
import { parsePathname } from '@/utils';
|
||||
import { postShareV1StatPage } from '@/request/ShareStat';
|
||||
import { getShareV1NodeList } from '@/request/ShareNode';
|
||||
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
||||
import { filterEmptyFolders, convertToTree } from '@/utils/drag';
|
||||
import { deepSearchFirstNode } from '@/utils';
|
||||
|
||||
const StatPage = {
|
||||
welcome: 1,
|
||||
node: 2,
|
||||
chat: 3,
|
||||
auth: 4,
|
||||
} as const;
|
||||
|
||||
const getFirstNode = async () => {
|
||||
const nodeListResult: any = await getShareV1NodeList();
|
||||
const tree = filterEmptyFolders(convertToTree(nodeListResult || []));
|
||||
return deepSearchFirstNode(tree);
|
||||
};
|
||||
|
||||
const getHomePath = async () => {
|
||||
const info = await getShareV1AppWebInfo();
|
||||
return info?.settings?.home_page_setting;
|
||||
};
|
||||
|
||||
const homeProxy = async (
|
||||
request: NextRequest,
|
||||
headers: Record<string, string>,
|
||||
session: string,
|
||||
) => {
|
||||
const url = request.nextUrl.clone();
|
||||
const { page, id } = parsePathname(url.pathname);
|
||||
try {
|
||||
// 获取节点列表
|
||||
if (url.pathname === '/') {
|
||||
const homePath = await getHomePath();
|
||||
if (homePath === 'custom') {
|
||||
return NextResponse.rewrite(new URL('/home', request.url));
|
||||
} else {
|
||||
const [firstNode] = await Promise.all([getFirstNode(), getHomePath()]);
|
||||
if (firstNode) {
|
||||
return NextResponse.rewrite(
|
||||
new URL(`/node/${firstNode.id}`, request.url),
|
||||
);
|
||||
}
|
||||
return NextResponse.rewrite(new URL('/node', request.url));
|
||||
}
|
||||
}
|
||||
|
||||
// 页面上报
|
||||
const pages = Object.keys(StatPage);
|
||||
if (pages.includes(page) || pages.includes(id)) {
|
||||
postShareV1StatPage(
|
||||
{
|
||||
scene: StatPage[page as keyof typeof StatPage],
|
||||
node_id: id || '',
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'x-pw-session-id': session,
|
||||
...headers,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
} catch (error) {
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'message' in error &&
|
||||
error.message === 'NEXT_REDIRECT'
|
||||
) {
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
`/auth/login?redirect=${encodeURIComponent(url.pathname + url.search)}`,
|
||||
request.url,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
};
|
||||
|
||||
const proxyShare = async (request: NextRequest) => {
|
||||
// 转发到 process.env.TARGET
|
||||
const kb_id = request.headers.get('x-kb-id') || process.env.DEV_KB_ID || '';
|
||||
|
||||
const targetOrigin = process.env.TARGET!;
|
||||
const targetUrl = new URL(
|
||||
request.nextUrl.pathname + request.nextUrl.search,
|
||||
targetOrigin,
|
||||
);
|
||||
// 构造 fetch 选项
|
||||
const fetchHeaders = new Headers(request.headers);
|
||||
fetchHeaders.set('x-kb-id', kb_id);
|
||||
|
||||
const hasBody = !['GET', 'HEAD'].includes(request.method);
|
||||
const fetchOptions: RequestInit = {
|
||||
method: request.method,
|
||||
headers: fetchHeaders,
|
||||
body: hasBody ? request.body : undefined,
|
||||
redirect: 'manual',
|
||||
...(hasBody && { duplex: 'half' as const }),
|
||||
};
|
||||
const proxyRes = await fetch(targetUrl.toString(), fetchOptions);
|
||||
const nextRes = new NextResponse(proxyRes.body, {
|
||||
status: proxyRes.status,
|
||||
headers: proxyRes.headers,
|
||||
statusText: proxyRes.statusText,
|
||||
});
|
||||
return nextRes;
|
||||
};
|
||||
|
||||
export async function proxy(request: NextRequest) {
|
||||
const url = request.nextUrl.clone();
|
||||
const pathname = url.pathname;
|
||||
if (pathname.startsWith('/widget')) {
|
||||
const widgetInfo: any = await getShareV1AppWidgetInfo();
|
||||
if (widgetInfo) {
|
||||
if (!widgetInfo?.settings?.widget_bot_settings?.is_open) {
|
||||
return NextResponse.rewrite(new URL('/not-found', request.url));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [key, value] of request.headers.entries()) {
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
let sessionId = request.cookies.get('x-pw-session-id')?.value || '';
|
||||
let needSetSessionId = false;
|
||||
|
||||
if (!sessionId) {
|
||||
sessionId = uuidv4();
|
||||
needSetSessionId = true;
|
||||
}
|
||||
|
||||
let response: NextResponse;
|
||||
|
||||
if (pathname.startsWith('/share/')) {
|
||||
response = await proxyShare(request);
|
||||
} else {
|
||||
response = await homeProxy(request, headers, sessionId);
|
||||
}
|
||||
|
||||
if (needSetSessionId) {
|
||||
response.cookies.set('x-pw-session-id', sessionId, {
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 年
|
||||
});
|
||||
}
|
||||
if (!pathname.startsWith('/share')) {
|
||||
response.headers.set('x-current-path', pathname);
|
||||
response.headers.set('x-current-search', url.search);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/',
|
||||
'/home',
|
||||
'/share/:path*',
|
||||
'/chat/:path*',
|
||||
'/widget',
|
||||
'/welcome',
|
||||
'/auth/login',
|
||||
'/node/:path*',
|
||||
'/node',
|
||||
],
|
||||
};
|
||||
|
|
@ -18,7 +18,6 @@ import {
|
|||
DomainOpenAICompletionsResponse,
|
||||
DomainResponse,
|
||||
PostShareV1ChatMessageParams,
|
||||
PostShareV1ChatWidgetParams,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
|
|
@ -92,28 +91,3 @@ export const postShareV1ChatMessage = (
|
|||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description ChatWidget
|
||||
*
|
||||
* @tags share_chat
|
||||
* @name PostShareV1ChatWidget
|
||||
* @summary ChatWidget
|
||||
* @request POST:/share/v1/chat/widget
|
||||
* @response `200` `DomainResponse` OK
|
||||
*/
|
||||
|
||||
export const postShareV1ChatWidget = (
|
||||
query: PostShareV1ChatWidgetParams,
|
||||
request: DomainChatRequest,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<DomainResponse>({
|
||||
path: `/share/v1/chat/widget`,
|
||||
method: "POST",
|
||||
query: query,
|
||||
body: request,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
/* 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 {
|
||||
DomainChatRequest,
|
||||
DomainChatSearchReq,
|
||||
DomainChatSearchResp,
|
||||
DomainResponse,
|
||||
PostShareV1ChatWidgetParams,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* @description ChatWidget
|
||||
*
|
||||
* @tags Widget
|
||||
* @name PostShareV1ChatWidget
|
||||
* @summary ChatWidget
|
||||
* @request POST:/share/v1/chat/widget
|
||||
* @response `200` `DomainResponse` OK
|
||||
*/
|
||||
|
||||
export const postShareV1ChatWidget = (
|
||||
query: PostShareV1ChatWidgetParams,
|
||||
request: DomainChatRequest,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<DomainResponse>({
|
||||
path: `/share/v1/chat/widget`,
|
||||
method: "POST",
|
||||
query: query,
|
||||
body: request,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description WidgetSearch
|
||||
*
|
||||
* @tags Widget
|
||||
* @name PostShareV1ChatWidgetSearch
|
||||
* @summary WidgetSearch
|
||||
* @request POST:/share/v1/chat/widget/search
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: DomainChatSearchResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postShareV1ChatWidgetSearch = (
|
||||
request: DomainChatSearchReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: DomainChatSearchResp;
|
||||
}
|
||||
>({
|
||||
path: `/share/v1/chat/widget/search`,
|
||||
method: "POST",
|
||||
body: request,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
|
@ -10,5 +10,6 @@ export * from './ShareNode'
|
|||
export * from './ShareOpenapi'
|
||||
export * from './ShareStat'
|
||||
export * from './Wechat'
|
||||
export * from './Widget'
|
||||
export * from './types'
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
||||
|
|
@ -206,6 +207,32 @@ export const postShareProV1AuthLdap = (
|
|||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 用户登出
|
||||
*
|
||||
* @tags ShareAuth
|
||||
* @name PostShareProV1AuthLogout
|
||||
* @summary 用户登出
|
||||
* @request POST:/share/pro/v1/auth/logout
|
||||
* @response `200` `(DomainPWResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postShareProV1AuthLogout = (params: RequestParams = {}) =>
|
||||
httpRequest<
|
||||
DomainPWResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp;
|
||||
}
|
||||
>({
|
||||
path: `/share/pro/v1/auth/logout`,
|
||||
method: "POST",
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description OAuth登录
|
||||
*
|
||||
|
|
|
|||
|
|
@ -52,10 +52,12 @@ export enum ConstsSourceType {
|
|||
export enum ConstsLicenseEdition {
|
||||
/** 开源版 */
|
||||
LicenseEditionFree = 0,
|
||||
/** 联创版 */
|
||||
LicenseEditionContributor = 1,
|
||||
/** 专业版 */
|
||||
LicenseEditionProfession = 1,
|
||||
/** 企业版 */
|
||||
LicenseEditionEnterprise = 2,
|
||||
/** 商业版 */
|
||||
LicenseEditionBusiness = 3,
|
||||
}
|
||||
|
||||
export enum ConstsContributeType {
|
||||
|
|
@ -455,6 +457,11 @@ export type GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp = Record<
|
|||
any
|
||||
>;
|
||||
|
||||
export type GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp = Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
||||
kb_id?: string;
|
||||
redirect_url?: string;
|
||||
|
|
@ -669,8 +676,6 @@ export interface GetApiProV1TokenListParams {
|
|||
}
|
||||
|
||||
export interface PostApiV1LicensePayload {
|
||||
/** license edition */
|
||||
license_edition: "contributor" | "enterprise";
|
||||
/** license type */
|
||||
license_type: "file" | "code";
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -171,14 +171,21 @@ export enum ConstsNodeAccessPerm {
|
|||
NodeAccessPermClosed = "closed",
|
||||
}
|
||||
|
||||
export enum ConstsModelSettingMode {
|
||||
ModelSettingModeManual = "manual",
|
||||
ModelSettingModeAuto = "auto",
|
||||
}
|
||||
|
||||
/** @format int32 */
|
||||
export enum ConstsLicenseEdition {
|
||||
/** 开源版 */
|
||||
LicenseEditionFree = 0,
|
||||
/** 联创版 */
|
||||
LicenseEditionContributor = 1,
|
||||
/** 专业版 */
|
||||
LicenseEditionProfession = 1,
|
||||
/** 企业版 */
|
||||
LicenseEditionEnterprise = 2,
|
||||
/** 商业版 */
|
||||
LicenseEditionBusiness = 3,
|
||||
}
|
||||
|
||||
export enum ConstsHomePageSetting {
|
||||
|
|
@ -922,6 +929,17 @@ export interface DomainMetricsConfig {
|
|||
type?: string;
|
||||
}
|
||||
|
||||
export interface DomainModelModeSetting {
|
||||
/** 百智云 API Key */
|
||||
auto_mode_api_key?: string;
|
||||
/** 自定义对话模型名称 */
|
||||
chat_model?: string;
|
||||
/** 手动模式下嵌入模型是否更新 */
|
||||
is_manual_embedding_updated?: boolean;
|
||||
/** 模式: manual 或 auto */
|
||||
mode?: ConstsModelSettingMode;
|
||||
}
|
||||
|
||||
export interface DomainMoveNodeReq {
|
||||
id: string;
|
||||
kb_id: string;
|
||||
|
|
@ -1171,6 +1189,17 @@ export interface DomainShareConversationMessage {
|
|||
role?: SchemaRoleType;
|
||||
}
|
||||
|
||||
export interface DomainShareNodeListItemResp {
|
||||
emoji?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
parent_id?: string;
|
||||
permissions?: DomainNodePermissions;
|
||||
position?: number;
|
||||
type?: DomainNodeType;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface DomainSimpleAuth {
|
||||
enabled?: boolean;
|
||||
password?: string;
|
||||
|
|
@ -1195,6 +1224,18 @@ export interface DomainStatPageReq {
|
|||
scene: 1 | 2 | 3 | 4;
|
||||
}
|
||||
|
||||
export interface DomainSwitchModeReq {
|
||||
/** 百智云 API Key */
|
||||
auto_mode_api_key?: string;
|
||||
/** 自定义对话模型名称 */
|
||||
chat_model?: string;
|
||||
mode: "manual" | "auto";
|
||||
}
|
||||
|
||||
export interface DomainSwitchModeResp {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface DomainTextConfig {
|
||||
title?: string;
|
||||
type?: string;
|
||||
|
|
@ -1336,11 +1377,18 @@ export interface DomainWecomAIBotSettings {
|
|||
}
|
||||
|
||||
export interface DomainWidgetBotSettings {
|
||||
btn_id?: string;
|
||||
btn_logo?: string;
|
||||
btn_position?: string;
|
||||
btn_style?: string;
|
||||
btn_text?: string;
|
||||
disclaimer?: string;
|
||||
is_open?: boolean;
|
||||
modal_position?: string;
|
||||
placeholder?: string;
|
||||
recommend_node_ids?: string[];
|
||||
recommend_questions?: string[];
|
||||
search_mode?: string;
|
||||
theme_mode?: string;
|
||||
}
|
||||
|
||||
|
|
@ -1646,6 +1694,7 @@ export interface V1ShareNodeDetailResp {
|
|||
editor_id?: string;
|
||||
id?: string;
|
||||
kb_id?: string;
|
||||
list?: DomainShareNodeListItemResp[];
|
||||
meta?: DomainNodeMeta;
|
||||
name?: string;
|
||||
parent_id?: string;
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ const DocContent = ({
|
|||
setCommentImages([]);
|
||||
message.success(
|
||||
appDetail?.web_app_comment_settings?.moderation_enable
|
||||
? '正在审核中...'
|
||||
? '评论已提交,请耐心等待审核'
|
||||
: '评论成功',
|
||||
);
|
||||
} catch (error: any) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue