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 {
|
type ShareNodeDetailResp struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
KbID string `json:"kb_id"`
|
KbID string `json:"kb_id"`
|
||||||
Type domain.NodeType `json:"type"`
|
Type domain.NodeType `json:"type"`
|
||||||
Status domain.NodeStatus `json:"status"`
|
Status domain.NodeStatus `json:"status"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Meta domain.NodeMeta `json:"meta"`
|
Meta domain.NodeMeta `json:"meta"`
|
||||||
ParentID string `json:"parent_id"`
|
ParentID string `json:"parent_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Permissions domain.NodePermissions `json:"permissions"`
|
Permissions domain.NodePermissions `json:"permissions"`
|
||||||
CreatorId string `json:"creator_id"`
|
CreatorId string `json:"creator_id"`
|
||||||
EditorId string `json:"editor_id"`
|
EditorId string `json:"editor_id"`
|
||||||
PublisherId string `json:"publisher_id"`
|
PublisherId string `json:"publisher_id"`
|
||||||
CreatorAccount string `json:"creator_account"`
|
CreatorAccount string `json:"creator_account"`
|
||||||
EditorAccount string `json:"editor_account"`
|
EditorAccount string `json:"editor_account"`
|
||||||
PublisherAccount string `json:"publisher_account"`
|
PublisherAccount string `json:"publisher_account"`
|
||||||
|
List []*domain.ShareNodeListItemResp `json:"list" gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build wireinject
|
//go:build wireinject
|
||||||
// +build wireinject
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build wireinject
|
//go:build wireinject
|
||||||
// +build wireinject
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
//go:build wireinject
|
//go:build wireinject
|
||||||
// +build wireinject
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
package consts
|
package consts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -13,28 +11,13 @@ const ContextKeyEdition contextKey = "edition"
|
||||||
type LicenseEdition int32
|
type LicenseEdition int32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
LicenseEditionFree LicenseEdition = 0 // 开源版
|
LicenseEditionFree LicenseEdition = 0 // 开源版
|
||||||
LicenseEditionContributor LicenseEdition = 1 // 联创版
|
LicenseEditionProfession LicenseEdition = 1 // 专业版
|
||||||
LicenseEditionEnterprise LicenseEdition = 2 // 企业版
|
LicenseEditionEnterprise LicenseEdition = 2 // 企业版
|
||||||
|
LicenseEditionBusiness LicenseEdition = 3 // 商业版
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetLicenseEdition(c echo.Context) LicenseEdition {
|
func GetLicenseEdition(c echo.Context) LicenseEdition {
|
||||||
edition, _ := c.Get("edition").(LicenseEdition)
|
edition, _ := c.Get("edition").(LicenseEdition)
|
||||||
return edition
|
return edition
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e LicenseEdition) GetMaxAuth(sourceType SourceType) int {
|
|
||||||
switch e {
|
|
||||||
case LicenseEditionFree:
|
|
||||||
if sourceType == SourceTypeGitHub {
|
|
||||||
return 10
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
case LicenseEditionContributor:
|
|
||||||
return 10
|
|
||||||
case LicenseEditionEnterprise:
|
|
||||||
return math.MaxInt
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -3478,7 +3478,7 @@ const docTemplate = `{
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"share_chat"
|
"Widget"
|
||||||
],
|
],
|
||||||
"summary": "ChatWidget",
|
"summary": "ChatWidget",
|
||||||
"parameters": [
|
"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": {
|
"/share/v1/comment": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "CreateComment",
|
"description": "CreateComment",
|
||||||
|
|
@ -4067,22 +4113,26 @@ const docTemplate = `{
|
||||||
"enum": [
|
"enum": [
|
||||||
0,
|
0,
|
||||||
1,
|
1,
|
||||||
2
|
2,
|
||||||
|
3
|
||||||
],
|
],
|
||||||
"x-enum-comments": {
|
"x-enum-comments": {
|
||||||
"LicenseEditionContributor": "联创版",
|
"LicenseEditionBusiness": "商业版",
|
||||||
"LicenseEditionEnterprise": "企业版",
|
"LicenseEditionEnterprise": "企业版",
|
||||||
"LicenseEditionFree": "开源版"
|
"LicenseEditionFree": "开源版",
|
||||||
|
"LicenseEditionProfession": "专业版"
|
||||||
},
|
},
|
||||||
"x-enum-descriptions": [
|
"x-enum-descriptions": [
|
||||||
"开源版",
|
"开源版",
|
||||||
"联创版",
|
"专业版",
|
||||||
"企业版"
|
"企业版",
|
||||||
|
"商业版"
|
||||||
],
|
],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"LicenseEditionFree",
|
"LicenseEditionFree",
|
||||||
"LicenseEditionContributor",
|
"LicenseEditionProfession",
|
||||||
"LicenseEditionEnterprise"
|
"LicenseEditionEnterprise",
|
||||||
|
"LicenseEditionBusiness"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"consts.ModelSettingMode": {
|
"consts.ModelSettingMode": {
|
||||||
|
|
@ -6311,6 +6361,9 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.MessageContent": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"domain.MessageFrom": {
|
"domain.MessageFrom": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -6712,6 +6765,9 @@ const docTemplate = `{
|
||||||
"stream": {
|
"stream": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"stream_options": {
|
||||||
|
"$ref": "#/definitions/domain.OpenAIStreamOptions"
|
||||||
|
},
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
|
@ -6834,7 +6890,7 @@ const docTemplate = `{
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
"type": "string"
|
"$ref": "#/definitions/domain.MessageContent"
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -6864,6 +6920,14 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.OpenAIStreamOptions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"include_usage": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.OpenAITool": {
|
"domain.OpenAITool": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"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": {
|
"domain.SimpleAuth": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -7647,15 +7740,33 @@ const docTemplate = `{
|
||||||
"domain.WidgetBotSettings": {
|
"domain.WidgetBotSettings": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"btn_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"btn_logo": {
|
"btn_logo": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"btn_position": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"btn_style": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"btn_text": {
|
"btn_text": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"disclaimer": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"is_open": {
|
"is_open": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"modal_position": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"placeholder": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"recommend_node_ids": {
|
"recommend_node_ids": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|
@ -7668,6 +7779,9 @@ const docTemplate = `{
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"search_mode": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"theme_mode": {
|
"theme_mode": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|
@ -8536,6 +8650,12 @@ const docTemplate = `{
|
||||||
"kb_id": {
|
"kb_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"list": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.ShareNodeListItemResp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"$ref": "#/definitions/domain.NodeMeta"
|
"$ref": "#/definitions/domain.NodeMeta"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3471,7 +3471,7 @@
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"share_chat"
|
"Widget"
|
||||||
],
|
],
|
||||||
"summary": "ChatWidget",
|
"summary": "ChatWidget",
|
||||||
"parameters": [
|
"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": {
|
"/share/v1/comment": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "CreateComment",
|
"description": "CreateComment",
|
||||||
|
|
@ -4060,22 +4106,26 @@
|
||||||
"enum": [
|
"enum": [
|
||||||
0,
|
0,
|
||||||
1,
|
1,
|
||||||
2
|
2,
|
||||||
|
3
|
||||||
],
|
],
|
||||||
"x-enum-comments": {
|
"x-enum-comments": {
|
||||||
"LicenseEditionContributor": "联创版",
|
"LicenseEditionBusiness": "商业版",
|
||||||
"LicenseEditionEnterprise": "企业版",
|
"LicenseEditionEnterprise": "企业版",
|
||||||
"LicenseEditionFree": "开源版"
|
"LicenseEditionFree": "开源版",
|
||||||
|
"LicenseEditionProfession": "专业版"
|
||||||
},
|
},
|
||||||
"x-enum-descriptions": [
|
"x-enum-descriptions": [
|
||||||
"开源版",
|
"开源版",
|
||||||
"联创版",
|
"专业版",
|
||||||
"企业版"
|
"企业版",
|
||||||
|
"商业版"
|
||||||
],
|
],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"LicenseEditionFree",
|
"LicenseEditionFree",
|
||||||
"LicenseEditionContributor",
|
"LicenseEditionProfession",
|
||||||
"LicenseEditionEnterprise"
|
"LicenseEditionEnterprise",
|
||||||
|
"LicenseEditionBusiness"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"consts.ModelSettingMode": {
|
"consts.ModelSettingMode": {
|
||||||
|
|
@ -6304,6 +6354,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.MessageContent": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"domain.MessageFrom": {
|
"domain.MessageFrom": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|
@ -6705,6 +6758,9 @@
|
||||||
"stream": {
|
"stream": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"stream_options": {
|
||||||
|
"$ref": "#/definitions/domain.OpenAIStreamOptions"
|
||||||
|
},
|
||||||
"temperature": {
|
"temperature": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
|
@ -6827,7 +6883,7 @@
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
"type": "string"
|
"$ref": "#/definitions/domain.MessageContent"
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|
@ -6857,6 +6913,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.OpenAIStreamOptions": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"include_usage": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.OpenAITool": {
|
"domain.OpenAITool": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"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": {
|
"domain.SimpleAuth": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -7640,15 +7733,33 @@
|
||||||
"domain.WidgetBotSettings": {
|
"domain.WidgetBotSettings": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"btn_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"btn_logo": {
|
"btn_logo": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"btn_position": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"btn_style": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"btn_text": {
|
"btn_text": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"disclaimer": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"is_open": {
|
"is_open": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"modal_position": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"placeholder": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"recommend_node_ids": {
|
"recommend_node_ids": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|
@ -7661,6 +7772,9 @@
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"search_mode": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"theme_mode": {
|
"theme_mode": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|
@ -8529,6 +8643,12 @@
|
||||||
"kb_id": {
|
"kb_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"list": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.ShareNodeListItemResp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"$ref": "#/definitions/domain.NodeMeta"
|
"$ref": "#/definitions/domain.NodeMeta"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -117,20 +117,24 @@ definitions:
|
||||||
- 0
|
- 0
|
||||||
- 1
|
- 1
|
||||||
- 2
|
- 2
|
||||||
|
- 3
|
||||||
format: int32
|
format: int32
|
||||||
type: integer
|
type: integer
|
||||||
x-enum-comments:
|
x-enum-comments:
|
||||||
LicenseEditionContributor: 联创版
|
LicenseEditionBusiness: 商业版
|
||||||
LicenseEditionEnterprise: 企业版
|
LicenseEditionEnterprise: 企业版
|
||||||
LicenseEditionFree: 开源版
|
LicenseEditionFree: 开源版
|
||||||
|
LicenseEditionProfession: 专业版
|
||||||
x-enum-descriptions:
|
x-enum-descriptions:
|
||||||
- 开源版
|
- 开源版
|
||||||
- 联创版
|
- 专业版
|
||||||
- 企业版
|
- 企业版
|
||||||
|
- 商业版
|
||||||
x-enum-varnames:
|
x-enum-varnames:
|
||||||
- LicenseEditionFree
|
- LicenseEditionFree
|
||||||
- LicenseEditionContributor
|
- LicenseEditionProfession
|
||||||
- LicenseEditionEnterprise
|
- LicenseEditionEnterprise
|
||||||
|
- LicenseEditionBusiness
|
||||||
consts.ModelSettingMode:
|
consts.ModelSettingMode:
|
||||||
enum:
|
enum:
|
||||||
- manual
|
- manual
|
||||||
|
|
@ -1610,6 +1614,8 @@ definitions:
|
||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
domain.MessageContent:
|
||||||
|
type: object
|
||||||
domain.MessageFrom:
|
domain.MessageFrom:
|
||||||
enum:
|
enum:
|
||||||
- 1
|
- 1
|
||||||
|
|
@ -1871,6 +1877,8 @@ definitions:
|
||||||
type: array
|
type: array
|
||||||
stream:
|
stream:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
stream_options:
|
||||||
|
$ref: '#/definitions/domain.OpenAIStreamOptions'
|
||||||
temperature:
|
temperature:
|
||||||
type: number
|
type: number
|
||||||
tool_choice:
|
tool_choice:
|
||||||
|
|
@ -1952,7 +1960,7 @@ definitions:
|
||||||
domain.OpenAIMessage:
|
domain.OpenAIMessage:
|
||||||
properties:
|
properties:
|
||||||
content:
|
content:
|
||||||
type: string
|
$ref: '#/definitions/domain.MessageContent'
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
role:
|
role:
|
||||||
|
|
@ -1973,6 +1981,11 @@ definitions:
|
||||||
required:
|
required:
|
||||||
- type
|
- type
|
||||||
type: object
|
type: object
|
||||||
|
domain.OpenAIStreamOptions:
|
||||||
|
properties:
|
||||||
|
include_usage:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
domain.OpenAITool:
|
domain.OpenAITool:
|
||||||
properties:
|
properties:
|
||||||
function:
|
function:
|
||||||
|
|
@ -2146,6 +2159,25 @@ definitions:
|
||||||
role:
|
role:
|
||||||
$ref: '#/definitions/schema.RoleType'
|
$ref: '#/definitions/schema.RoleType'
|
||||||
type: object
|
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:
|
domain.SimpleAuth:
|
||||||
properties:
|
properties:
|
||||||
enabled:
|
enabled:
|
||||||
|
|
@ -2488,12 +2520,24 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
domain.WidgetBotSettings:
|
domain.WidgetBotSettings:
|
||||||
properties:
|
properties:
|
||||||
|
btn_id:
|
||||||
|
type: string
|
||||||
btn_logo:
|
btn_logo:
|
||||||
type: string
|
type: string
|
||||||
|
btn_position:
|
||||||
|
type: string
|
||||||
|
btn_style:
|
||||||
|
type: string
|
||||||
btn_text:
|
btn_text:
|
||||||
type: string
|
type: string
|
||||||
|
disclaimer:
|
||||||
|
type: string
|
||||||
is_open:
|
is_open:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
modal_position:
|
||||||
|
type: string
|
||||||
|
placeholder:
|
||||||
|
type: string
|
||||||
recommend_node_ids:
|
recommend_node_ids:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -2502,6 +2546,8 @@ definitions:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
search_mode:
|
||||||
|
type: string
|
||||||
theme_mode:
|
theme_mode:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
|
@ -3075,6 +3121,10 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
kb_id:
|
kb_id:
|
||||||
type: string
|
type: string
|
||||||
|
list:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/domain.ShareNodeListItemResp'
|
||||||
|
type: array
|
||||||
meta:
|
meta:
|
||||||
$ref: '#/definitions/domain.NodeMeta'
|
$ref: '#/definitions/domain.NodeMeta'
|
||||||
name:
|
name:
|
||||||
|
|
@ -5296,7 +5346,34 @@ paths:
|
||||||
$ref: '#/definitions/domain.Response'
|
$ref: '#/definitions/domain.Response'
|
||||||
summary: ChatWidget
|
summary: ChatWidget
|
||||||
tags:
|
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:
|
/share/v1/comment:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
||||||
|
|
@ -405,6 +405,13 @@ type WidgetBotSettings struct {
|
||||||
BtnLogo string `json:"btn_logo,omitempty"`
|
BtnLogo string `json:"btn_logo,omitempty"`
|
||||||
RecommendQuestions []string `json:"recommend_questions,omitempty"`
|
RecommendQuestions []string `json:"recommend_questions,omitempty"`
|
||||||
RecommendNodeIDs []string `json:"recommend_node_ids,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 {
|
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 表示内容数组中的单个元素
|
// OpenAIContentPart 表示内容数组中的单个元素
|
||||||
type OpenAIContentPart struct {
|
type OpenAIContentPart struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Text string `json:"text,omitempty"`
|
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 格式
|
// UnmarshalJSON 自定义解析,支持 string 或 array 格式
|
||||||
|
|
@ -63,7 +69,7 @@ func (mc *MessageContent) UnmarshalJSON(data []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON 自定义序列化
|
// MarshalJSON 自定义序列化
|
||||||
func (mc MessageContent) MarshalJSON() ([]byte, error) {
|
func (mc *MessageContent) MarshalJSON() ([]byte, error) {
|
||||||
if mc.isString {
|
if mc.isString {
|
||||||
return json.Marshal(mc.strValue)
|
return json.Marshal(mc.strValue)
|
||||||
}
|
}
|
||||||
|
|
@ -93,9 +99,9 @@ func (mc *MessageContent) String() string {
|
||||||
}
|
}
|
||||||
// 从数组中提取文本
|
// 从数组中提取文本
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
for i, part := range mc.arrValue {
|
for _, part := range mc.arrValue {
|
||||||
if part.Type == "text" {
|
if part.Type == "text" {
|
||||||
if i > 0 && part.Text != "" {
|
if builder.Len() > 0 && part.Text != "" {
|
||||||
builder.WriteString(" ")
|
builder.WriteString(" ")
|
||||||
}
|
}
|
||||||
builder.WriteString(part.Text)
|
builder.WriteString(part.Text)
|
||||||
|
|
@ -181,9 +187,9 @@ type OpenAIStreamResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenAIStreamChoice struct {
|
type OpenAIStreamChoice struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
Delta OpenAIMessage `json:"delta"`
|
Delta OpenAIMessage `json:"delta"`
|
||||||
FinishReason *string `json:"finish_reason,omitempty"`
|
FinishReason *string `json:"finish_reason,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenAI 错误响应结构体
|
// OpenAI 错误响应结构体
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ func NewShareChatHandler(
|
||||||
share.POST("/search", h.ChatSearch, h.ShareAuthMiddleware.Authorize)
|
share.POST("/search", h.ChatSearch, h.ShareAuthMiddleware.Authorize)
|
||||||
share.POST("/completions", h.ChatCompletions)
|
share.POST("/completions", h.ChatCompletions)
|
||||||
share.POST("/widget", h.ChatWidget)
|
share.POST("/widget", h.ChatWidget)
|
||||||
|
share.POST("/widget/search", h.WidgetSearch)
|
||||||
share.POST("/feedback", h.FeedBack)
|
share.POST("/feedback", h.FeedBack)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +132,7 @@ func (h *ShareChatHandler) ChatMessage(c echo.Context) error {
|
||||||
//
|
//
|
||||||
// @Summary ChatWidget
|
// @Summary ChatWidget
|
||||||
// @Description ChatWidget
|
// @Description ChatWidget
|
||||||
// @Tags share_chat
|
// @Tags Widget
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param app_type query string true "app type"
|
// @Param app_type query string true "app type"
|
||||||
|
|
@ -444,7 +445,7 @@ func stringPtr(s string) *string {
|
||||||
return &s
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatMessage chat search
|
// ChatSearch searches chat messages in shared knowledge base
|
||||||
//
|
//
|
||||||
// @Summary ChatSearch
|
// @Summary ChatSearch
|
||||||
// @Description ChatSearch
|
// @Description ChatSearch
|
||||||
|
|
@ -487,3 +488,43 @@ func (h *ShareChatHandler) ChatSearch(c echo.Context) error {
|
||||||
}
|
}
|
||||||
return h.NewResponseWithData(c, resp)
|
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/labstack/echo/v4"
|
||||||
|
|
||||||
"github.com/chaitin/panda-wiki/consts"
|
|
||||||
"github.com/chaitin/panda-wiki/domain"
|
"github.com/chaitin/panda-wiki/domain"
|
||||||
"github.com/chaitin/panda-wiki/handler"
|
"github.com/chaitin/panda-wiki/handler"
|
||||||
"github.com/chaitin/panda-wiki/log"
|
"github.com/chaitin/panda-wiki/log"
|
||||||
|
|
@ -157,7 +156,7 @@ func (h *ShareCommentHandler) GetCommentList(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询数据库获取所有评论-->0 所有, 1,2 为需要审核的评论
|
// 查询数据库获取所有评论-->0 所有, 1,2 为需要审核的评论
|
||||||
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID, consts.GetLicenseEdition(c))
|
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return h.NewResponseWithError(c, "failed to get comment list", err)
|
return h.NewResponseWithError(c, "failed to get comment list", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,5 +91,15 @@ func (h *ShareNodeHandler) GetNodeDetail(c echo.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return h.NewResponseWithError(c, "failed to get node detail", err)
|
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)
|
return h.NewResponseWithData(c, node)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
v1 "github.com/chaitin/panda-wiki/api/kb/v1"
|
v1 "github.com/chaitin/panda-wiki/api/kb/v1"
|
||||||
"github.com/chaitin/panda-wiki/consts"
|
"github.com/chaitin/panda-wiki/consts"
|
||||||
|
"github.com/chaitin/panda-wiki/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KBUserList
|
// KBUserList
|
||||||
|
|
@ -55,8 +56,8 @@ func (h *KnowledgeBaseHandler) KBUserInvite(c echo.Context) error {
|
||||||
return h.NewResponseWithError(c, "validate request failed", err)
|
return h.NewResponseWithError(c, "validate request failed", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
|
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
|
||||||
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
|
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := h.usecase.KBUserInvite(c.Request().Context(), req)
|
err := h.usecase.KBUserInvite(c.Request().Context(), req)
|
||||||
|
|
@ -87,8 +88,8 @@ func (h *KnowledgeBaseHandler) KBUserUpdate(c echo.Context) error {
|
||||||
return h.NewResponseWithError(c, "validate request failed", err)
|
return h.NewResponseWithError(c, "validate request failed", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
|
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
|
||||||
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
|
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := h.usecase.UpdateUserKB(c.Request().Context(), req)
|
err := h.usecase.UpdateUserKB(c.Request().Context(), req)
|
||||||
|
|
|
||||||
|
|
@ -91,11 +91,7 @@ func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c echo.Context) error {
|
||||||
return h.NewResponseWithError(c, "ports is required", nil)
|
return h.NewResponseWithError(c, "ports is required", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.MaxKB = 1
|
req.MaxKB = domain.GetBaseEditionLimitation(c.Request().Context()).MaxKb
|
||||||
maxKB := c.Get("max_kb")
|
|
||||||
if maxKB != nil {
|
|
||||||
req.MaxKB = maxKB.(int)
|
|
||||||
}
|
|
||||||
|
|
||||||
did, err := h.usecase.CreateKnowledgeBase(c.Request().Context(), &req)
|
did, err := h.usecase.CreateKnowledgeBase(c.Request().Context(), &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func NewModelHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *
|
||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// get model list
|
// GetModelList
|
||||||
//
|
//
|
||||||
// @Summary get model list
|
// @Summary get model list
|
||||||
// @Description get model list
|
// @Description get model list
|
||||||
|
|
@ -66,7 +66,7 @@ func (h *ModelHandler) GetModelList(c echo.Context) error {
|
||||||
return h.NewResponseWithData(c, models)
|
return h.NewResponseWithData(c, models)
|
||||||
}
|
}
|
||||||
|
|
||||||
// create model
|
// CreateModel
|
||||||
//
|
//
|
||||||
// @Summary create model
|
// @Summary create model
|
||||||
// @Description create model
|
// @Description create model
|
||||||
|
|
@ -85,9 +85,6 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
|
||||||
return h.NewResponseWithError(c, "invalid request", err)
|
return h.NewResponseWithError(c, "invalid request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
|
|
||||||
return h.NewResponseWithError(c, "联创版只能使用百智云模型哦~", nil)
|
|
||||||
}
|
|
||||||
ctx := c.Request().Context()
|
ctx := c.Request().Context()
|
||||||
|
|
||||||
param := domain.ModelParam{}
|
param := domain.ModelParam{}
|
||||||
|
|
@ -112,7 +109,7 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
|
||||||
return h.NewResponseWithData(c, model)
|
return h.NewResponseWithData(c, model)
|
||||||
}
|
}
|
||||||
|
|
||||||
// update model
|
// UpdateModel
|
||||||
//
|
//
|
||||||
// @Description update model
|
// @Description update model
|
||||||
// @Tags model
|
// @Tags model
|
||||||
|
|
@ -130,9 +127,6 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
|
||||||
return h.NewResponseWithError(c, "invalid request", err)
|
return h.NewResponseWithError(c, "invalid request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
|
|
||||||
return h.NewResponseWithError(c, "联创版只能使用百智云模型哦~", nil)
|
|
||||||
}
|
|
||||||
ctx := c.Request().Context()
|
ctx := c.Request().Context()
|
||||||
if err := h.usecase.Update(ctx, &req); err != nil {
|
if err := h.usecase.Update(ctx, &req); err != nil {
|
||||||
return h.NewResponseWithError(c, "update model failed", err)
|
return h.NewResponseWithError(c, "update model failed", err)
|
||||||
|
|
@ -140,7 +134,7 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
|
||||||
return h.NewResponseWithData(c, nil)
|
return h.NewResponseWithData(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check model
|
// CheckModel
|
||||||
//
|
//
|
||||||
// @Summary check model
|
// @Summary check model
|
||||||
// @Description check model
|
// @Description check model
|
||||||
|
|
|
||||||
|
|
@ -81,15 +81,13 @@ func (h *NodeHandler) CreateNode(c echo.Context) error {
|
||||||
if err := c.Validate(req); err != nil {
|
if err := c.Validate(req); err != nil {
|
||||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||||
}
|
}
|
||||||
req.MaxNode = 300
|
|
||||||
if maxNode := c.Get("max_node"); maxNode != nil {
|
req.MaxNode = domain.GetBaseEditionLimitation(ctx).MaxNode
|
||||||
req.MaxNode = maxNode.(int)
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := h.usecase.Create(c.Request().Context(), req, authInfo.UserId)
|
id, err := h.usecase.Create(c.Request().Context(), req, authInfo.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, domain.ErrMaxNodeLimitReached) {
|
if errors.Is(err, domain.ErrMaxNodeLimitReached) {
|
||||||
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到联创版或企业版", nil)
|
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到更高版本", nil)
|
||||||
}
|
}
|
||||||
return h.NewResponseWithError(c, "create node failed", err)
|
return h.NewResponseWithError(c, "create node failed", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ type AuthMiddleware interface {
|
||||||
Authorize(next echo.HandlerFunc) echo.HandlerFunc
|
Authorize(next echo.HandlerFunc) echo.HandlerFunc
|
||||||
ValidateUserRole(role consts.UserRole) echo.MiddlewareFunc
|
ValidateUserRole(role consts.UserRole) echo.MiddlewareFunc
|
||||||
ValidateKBUserPerm(role consts.UserKBPermission) echo.MiddlewareFunc
|
ValidateKBUserPerm(role consts.UserKBPermission) echo.MiddlewareFunc
|
||||||
ValidateLicenseEdition(edition consts.LicenseEdition) echo.MiddlewareFunc
|
ValidateLicenseEdition(edition ...consts.LicenseEdition) echo.MiddlewareFunc
|
||||||
MustGetUserID(c echo.Context) (string, bool)
|
MustGetUserID(c echo.Context) (string, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
|
@ -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(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
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{
|
return c.JSON(http.StatusForbidden, domain.PWResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
Message: "Unauthorized ValidateLicenseEdition",
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if int(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, licenseEdition.GetMaxAuth(sourceType))
|
return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, domain.GetBaseEditionLimitation(ctx).MaxSSOUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.LastLoginTime = time.Now()
|
auth.LastLoginTime = time.Now()
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,12 @@ func (r *CommentRepository) CreateComment(ctx context.Context, comment *domain.C
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string, edition consts.LicenseEdition) ([]*domain.ShareCommentListItem, int64, error) {
|
func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string) ([]*domain.ShareCommentListItem, int64, error) {
|
||||||
// 按照时间排序来查询node_id的comments
|
// 按照时间排序来查询node_id的comments
|
||||||
var comments []*domain.ShareCommentListItem
|
var comments []*domain.ShareCommentListItem
|
||||||
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("node_id = ?", nodeID)
|
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("node_id = ?", nodeID)
|
||||||
|
|
||||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
|
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit {
|
||||||
query = query.Where("status = ?", domain.CommentStatusAccepted) //accepted
|
query = query.Where("status = ?", domain.CommentStatusAccepted) //accepted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,14 +50,14 @@ func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string, e
|
||||||
|
|
||||||
func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domain.CommentListReq, edition consts.LicenseEdition) ([]*domain.CommentListItem, int64, error) {
|
func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domain.CommentListReq, edition consts.LicenseEdition) ([]*domain.CommentListItem, int64, error) {
|
||||||
comments := []*domain.CommentListItem{}
|
comments := []*domain.CommentListItem{}
|
||||||
query := r.db.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
|
var count int64
|
||||||
if req.Status == nil {
|
if req.Status == nil {
|
||||||
if err := query.Count(&count).Error; err != nil {
|
if err := query.Count(&count).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
|
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit {
|
||||||
query = query.Where("comments.status = ?", *req.Status)
|
query = query.Where("comments.status = ?", *req.Status)
|
||||||
}
|
}
|
||||||
// 按照时间排序来查询kb_id的comments ->reject pending accepted
|
// 按照时间排序来查询kb_id的comments ->reject pending accepted
|
||||||
|
|
@ -84,7 +84,7 @@ func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domai
|
||||||
|
|
||||||
func (r *CommentRepository) DeleteCommentList(ctx context.Context, commentID []string) error {
|
func (r *CommentRepository) DeleteCommentList(ctx context.Context, commentID []string) error {
|
||||||
// 批量删除指定id的comment,获取删除的总的数量、
|
// 批量删除指定id的comment,获取删除的总的数量、
|
||||||
query := r.db.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 {
|
if err := query.Delete(&domain.Comment{}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -341,11 +341,12 @@ func (r *KnowledgeBaseRepository) CreateKnowledgeBase(ctx context.Context, maxKB
|
||||||
Name: kb.Name,
|
Name: kb.Name,
|
||||||
Type: domain.AppTypeWeb,
|
Type: domain.AppTypeWeb,
|
||||||
Settings: domain.AppSettings{
|
Settings: domain.AppSettings{
|
||||||
Title: kb.Name,
|
Title: kb.Name,
|
||||||
Desc: kb.Name,
|
Desc: kb.Name,
|
||||||
Keyword: kb.Name,
|
Keyword: kb.Name,
|
||||||
Icon: domain.DefaultPandaWikiIconB64,
|
AutoSitemap: true,
|
||||||
WelcomeStr: fmt.Sprintf("欢迎使用%s", kb.Name),
|
Icon: domain.DefaultPandaWikiIconB64,
|
||||||
|
WelcomeStr: fmt.Sprintf("欢迎使用%s", kb.Name),
|
||||||
Btns: []any{
|
Btns: []any{
|
||||||
AppBtn{
|
AppBtn{
|
||||||
ID: uuid.New().String(),
|
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.kb_id = ?", kbID).
|
||||||
Where("kb_release_node_releases.release_id = ?", kbRelease.ID).
|
Where("kb_release_node_releases.release_id = ?", kbRelease.ID).
|
||||||
Where("nodes.permissions->>'visible' != ?", consts.NodeAccessPermClosed).
|
Where("nodes.permissions->>'visible' != ?", consts.NodeAccessPermClosed).
|
||||||
Select("node_releases.node_id as id, node_releases.name, node_releases.type, node_releases.parent_id, 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 {
|
Find(&nodes).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,18 +60,14 @@ func (r *UserRepository) CreateUser(ctx context.Context, user *domain.User, edit
|
||||||
}
|
}
|
||||||
user.Password = string(hashedPassword)
|
user.Password = string(hashedPassword)
|
||||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionFree {
|
var count int64
|
||||||
var count int64
|
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
|
||||||
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
if edition == consts.LicenseEditionFree && count >= 1 {
|
|
||||||
return errors.New("free edition only allows 1 user")
|
|
||||||
}
|
|
||||||
if edition == consts.LicenseEditionContributor && count >= 5 {
|
|
||||||
return errors.New("contributor edition only allows 5 user")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if 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 {
|
if err := tx.Create(user).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,34 +88,38 @@ func NewAppUsecase(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *AppUsecase) ValidateUpdateApp(ctx context.Context, id string, req *domain.UpdateAppReq, edition consts.LicenseEdition) error {
|
func (u *AppUsecase) ValidateUpdateApp(ctx context.Context, id string, req *domain.UpdateAppReq, edition consts.LicenseEdition) error {
|
||||||
switch edition {
|
app, err := u.repo.GetAppDetail(ctx, id)
|
||||||
case consts.LicenseEditionFree:
|
if err != nil {
|
||||||
app, err := u.repo.GetAppDetail(ctx, id)
|
return err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -618,8 +622,8 @@ func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId
|
||||||
}
|
}
|
||||||
showBrand := true
|
showBrand := true
|
||||||
defaultDisclaimer := "本回答由 PandaWiki 基于 AI 生成,仅供参考。"
|
defaultDisclaimer := "本回答由 PandaWiki 基于 AI 生成,仅供参考。"
|
||||||
licenseEdition, _ := ctx.Value(consts.ContextKeyEdition).(consts.LicenseEdition)
|
|
||||||
if licenseEdition < consts.LicenseEditionEnterprise {
|
if !domain.GetBaseEditionLimitation(ctx).AllowCustomCopyright {
|
||||||
appInfo.Settings.WebAppCustomSettings.ShowBrandInfo = &showBrand
|
appInfo.Settings.WebAppCustomSettings.ShowBrandInfo = &showBrand
|
||||||
appInfo.Settings.DisclaimerSettings.Content = &defaultDisclaimer
|
appInfo.Settings.DisclaimerSettings.Content = &defaultDisclaimer
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,8 @@ func (u *CommentUsecase) CreateComment(ctx context.Context, commentReq *domain.C
|
||||||
return CommentStr, nil
|
return CommentStr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *CommentUsecase) GetCommentListByNodeID(ctx context.Context, nodeID string, edition consts.LicenseEdition) (*domain.PaginatedResult[[]*domain.ShareCommentListItem], error) {
|
func (u *CommentUsecase) GetCommentListByNodeID(ctx context.Context, nodeID string) (*domain.PaginatedResult[[]*domain.ShareCommentListItem], error) {
|
||||||
comments, total, err := u.CommentRepo.GetCommentList(ctx, nodeID, edition)
|
comments, total, err := u.CommentRepo.GetCommentList(ctx, nodeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,56 @@ func (u *NodeUsecase) GetNodeReleaseListByKBID(ctx context.Context, kbID string,
|
||||||
return items, nil
|
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) {
|
func (u *NodeUsecase) GetNodeIdsByAuthId(ctx context.Context, authId uint, PermName consts.NodePermName) ([]string, error) {
|
||||||
authGroups, err := u.authRepo.GetAuthGroupWithParentsByAuthId(ctx, authId)
|
authGroups, err := u.authRepo.GetAuthGroupWithParentsByAuthId(ctx, authId)
|
||||||
if err != nil {
|
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 {
|
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 {
|
if req.Permissions.Answerable == consts.NodeAccessPermPartial || req.Permissions.Visitable == consts.NodeAccessPermPartial || req.Permissions.Visible == consts.NodeAccessPermPartial {
|
||||||
return domain.ErrPermissionDenied
|
return domain.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
|
@ -67,12 +68,12 @@ func (u *StatUseCase) ValidateStatDay(statDay consts.StatDay, edition consts.Lic
|
||||||
case consts.StatDay1:
|
case consts.StatDay1:
|
||||||
return nil
|
return nil
|
||||||
case consts.StatDay7:
|
case consts.StatDay7:
|
||||||
if edition < consts.LicenseEditionContributor {
|
if edition == consts.LicenseEditionFree {
|
||||||
return domain.ErrPermissionDenied
|
return domain.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case consts.StatDay30, consts.StatDay90:
|
case consts.StatDay30, consts.StatDay90:
|
||||||
if edition < consts.LicenseEditionEnterprise {
|
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) {
|
||||||
return domain.ErrPermissionDenied
|
return domain.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -588,6 +588,7 @@ export type ChatConversationItem = {
|
||||||
export type ChatConversationPair = {
|
export type ChatConversationPair = {
|
||||||
user: string;
|
user: string;
|
||||||
assistant: string;
|
assistant: string;
|
||||||
|
thinking_content: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
info: {
|
info: {
|
||||||
feedback_content: string;
|
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();
|
callback();
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [subscribe]);
|
}, [subscribe, appPreviewData, id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledCommonWrapper>
|
<StyledCommonWrapper>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import { setAppPreviewData } from '@/store/slices/config';
|
||||||
import { DomainSocialMediaAccount } from '@/request/types';
|
import { DomainSocialMediaAccount } from '@/request/types';
|
||||||
import Switch from '../basicComponents/Switch';
|
import Switch from '../basicComponents/Switch';
|
||||||
import DragSocialInfo from '../basicComponents/DragSocialInfo';
|
import DragSocialInfo from '../basicComponents/DragSocialInfo';
|
||||||
|
import VersionMask from '@/components/VersionMask';
|
||||||
|
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||||
|
|
||||||
interface FooterConfigProps {
|
interface FooterConfigProps {
|
||||||
data?: AppDetail | null;
|
data?: AppDetail | null;
|
||||||
|
|
@ -75,9 +77,6 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
||||||
);
|
);
|
||||||
const footer_show_intro = watch('footer_show_intro');
|
const footer_show_intro = watch('footer_show_intro');
|
||||||
|
|
||||||
const isEnterprise = useMemo(() => {
|
|
||||||
return license.edition === 2;
|
|
||||||
}, [license]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEdit && appPreviewData) {
|
if (isEdit && appPreviewData) {
|
||||||
setValue(
|
setValue(
|
||||||
|
|
@ -506,29 +505,33 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
{isEnterprise && (
|
|
||||||
<Stack direction={'column'} gap={2}>
|
<Stack direction={'column'} gap={2}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
'&::before': {
|
'&::before': {
|
||||||
content: '""',
|
content: '""',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
width: 4,
|
width: 4,
|
||||||
height: 12,
|
height: 12,
|
||||||
bgcolor: '#3248F2',
|
bgcolor: '#3248F2',
|
||||||
borderRadius: '2px',
|
borderRadius: '2px',
|
||||||
mr: 1,
|
mr: 1,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
PandaWiki 版权信息
|
PandaWiki 版权信息
|
||||||
</Box>
|
</Box>
|
||||||
|
<VersionMask
|
||||||
|
permission={PROFESSION_VERSION_PERMISSION}
|
||||||
|
sx={{ inset: '-8px 0' }}
|
||||||
|
>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='show_brand_info'
|
name='show_brand_info'
|
||||||
|
|
@ -548,7 +551,6 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
||||||
<Switch
|
<Switch
|
||||||
sx={{ marginLeft: 'auto' }}
|
sx={{ marginLeft: 'auto' }}
|
||||||
{...field}
|
{...field}
|
||||||
disabled={!isEnterprise}
|
|
||||||
checked={field?.value === false ? false : true}
|
checked={field?.value === false ? false : true}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
field.onChange(e.target.checked);
|
field.onChange(e.target.checked);
|
||||||
|
|
@ -558,8 +560,8 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</VersionMask>
|
||||||
)}
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { KnowledgeBaseListItem } from '@/api';
|
import { KnowledgeBaseListItem } from '@/api';
|
||||||
import { useURLSearchParams } from '@/hooks';
|
import { useURLSearchParams } from '@/hooks';
|
||||||
|
import { useFeatureValue } from '@/hooks';
|
||||||
import { ConstsUserRole } from '@/request/types';
|
import { ConstsUserRole } from '@/request/types';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store';
|
import { useAppDispatch, useAppSelector } from '@/store';
|
||||||
import { setKbC, setKbId } from '@/store/slices/config';
|
import { setKbC, setKbId } from '@/store/slices/config';
|
||||||
|
|
@ -23,14 +24,14 @@ const KBSelect = () => {
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [_, setSearchParams] = useURLSearchParams();
|
const [_, setSearchParams] = useURLSearchParams();
|
||||||
const { kb_id, kbList, license, user } = useAppSelector(
|
const { kb_id, kbList, user } = useAppSelector(state => state.config);
|
||||||
state => state.config,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [modifyOpen, setModifyOpen] = useState(false);
|
const [modifyOpen, setModifyOpen] = useState(false);
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const [opraData, setOpraData] = useState<KnowledgeBaseListItem | null>(null);
|
const [opraData, setOpraData] = useState<KnowledgeBaseListItem | null>(null);
|
||||||
|
|
||||||
|
const wikiCount = useFeatureValue('wikiCount');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(kbList || []).length > 0 && (
|
{(kbList || []).length > 0 && (
|
||||||
|
|
@ -121,8 +122,7 @@ const KBSelect = () => {
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={
|
disabled={
|
||||||
(license.edition === 0 && (kbList || []).length >= 1) ||
|
(kbList || []).length >= wikiCount ||
|
||||||
(license.edition === 1 && (kbList || []).length >= 3) ||
|
|
||||||
user.role === ConstsUserRole.UserRoleUser
|
user.role === ConstsUserRole.UserRoleUser
|
||||||
}
|
}
|
||||||
onClick={event => {
|
onClick={event => {
|
||||||
|
|
|
||||||
|
|
@ -3,27 +3,20 @@ import {
|
||||||
getApiV1License,
|
getApiV1License,
|
||||||
deleteApiV1License,
|
deleteApiV1License,
|
||||||
} from '@/request/pro/License';
|
} from '@/request/pro/License';
|
||||||
import { PostApiV1LicensePayload } from '@/request/pro/types';
|
|
||||||
import HelpCenter from '@/assets/json/help-center.json';
|
import HelpCenter from '@/assets/json/help-center.json';
|
||||||
import Takeoff from '@/assets/json/takeoff.json';
|
import Takeoff from '@/assets/json/takeoff.json';
|
||||||
import error from '@/assets/json/error.json';
|
import error from '@/assets/json/error.json';
|
||||||
import IconUpgrade from '@/assets/json/upgrade.json';
|
import IconUpgrade from '@/assets/json/upgrade.json';
|
||||||
import Upload from '@/components/UploadFile/Drag';
|
import Upload from '@/components/UploadFile/Drag';
|
||||||
import { EditionType } from '@/constant/enums';
|
import { useVersionInfo } from '@/hooks';
|
||||||
import { useAppDispatch, useAppSelector } from '@/store';
|
import { useAppDispatch, useAppSelector } from '@/store';
|
||||||
import { setLicense } from '@/store/slices/config';
|
import { setLicense } from '@/store/slices/config';
|
||||||
import {
|
import { Box, Button, IconButton, Stack, TextField } from '@mui/material';
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
MenuItem,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { CusTabs, Icon, message, Modal } from '@ctzhian/ui';
|
import { CusTabs, Icon, message, Modal } from '@ctzhian/ui';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import LottieIcon from '../LottieIcon';
|
import LottieIcon from '../LottieIcon';
|
||||||
|
import { ConstsLicenseEdition } from '@/request/types';
|
||||||
|
|
||||||
interface AuthTypeModalProps {
|
interface AuthTypeModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -42,10 +35,9 @@ const AuthTypeModal = ({
|
||||||
const { license } = useAppSelector(state => state.config);
|
const { license } = useAppSelector(state => state.config);
|
||||||
|
|
||||||
const [selected, setSelected] = useState<'file' | 'code'>(
|
const [selected, setSelected] = useState<'file' | 'code'>(
|
||||||
license.edition === 2 ? 'file' : 'code',
|
license.edition === ConstsLicenseEdition.LicenseEditionEnterprise
|
||||||
);
|
? 'file'
|
||||||
const [authVersion, setAuthVersion] = useState<'contributor' | 'enterprise'>(
|
: 'code',
|
||||||
license.edition === 2 ? 'enterprise' : 'contributor',
|
|
||||||
);
|
);
|
||||||
const [updateOpen, setUpdateOpen] = useState(false);
|
const [updateOpen, setUpdateOpen] = useState(false);
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
|
|
@ -53,16 +45,15 @@ const AuthTypeModal = ({
|
||||||
const [file, setFile] = useState<File | undefined>(undefined);
|
const [file, setFile] = useState<File | undefined>(undefined);
|
||||||
const [unbindLoading, setUnbindLoading] = useState(false);
|
const [unbindLoading, setUnbindLoading] = useState(false);
|
||||||
|
|
||||||
|
const versionInfo = useVersionInfo();
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const params: PostApiV1LicensePayload = {
|
setLoading(true);
|
||||||
license_edition: authVersion,
|
postApiV1License({
|
||||||
license_type: selected,
|
license_type: selected,
|
||||||
license_code: code,
|
license_code: code,
|
||||||
license_file: file,
|
license_file: file,
|
||||||
};
|
})
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
postApiV1License(params)
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
message.success('激活成功');
|
message.success('激活成功');
|
||||||
setUpdateOpen(false);
|
setUpdateOpen(false);
|
||||||
|
|
@ -148,10 +139,8 @@ const AuthTypeModal = ({
|
||||||
<Stack direction={'row'} alignItems={'center'}>
|
<Stack direction={'row'} alignItems={'center'}>
|
||||||
<Box sx={{ width: 120, flexShrink: 0 }}>产品型号</Box>
|
<Box sx={{ width: 120, flexShrink: 0 }}>产品型号</Box>
|
||||||
<Stack direction={'row'} alignItems={'center'} gap={2}>
|
<Stack direction={'row'} alignItems={'center'} gap={2}>
|
||||||
<Box sx={{ minWidth: 50 }}>
|
<Box sx={{ minWidth: 50 }}>{versionInfo.label}</Box>
|
||||||
{EditionType[license.edition as keyof typeof EditionType].text}
|
{license.edition === ConstsLicenseEdition.LicenseEditionFree ? (
|
||||||
</Box>
|
|
||||||
{license.edition === 0 ? (
|
|
||||||
<Stack direction={'row'} gap={2}>
|
<Stack direction={'row'} gap={2}>
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
|
|
@ -240,7 +229,7 @@ const AuthTypeModal = ({
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
{license.edition! > 0 && (
|
{license.edition! !== ConstsLicenseEdition.LicenseEditionFree && (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack direction={'row'} alignItems={'center'}>
|
<Stack direction={'row'} alignItems={'center'}>
|
||||||
<Box sx={{ width: 120, flexShrink: 0 }}>授权时间</Box>
|
<Box sx={{ width: 120, flexShrink: 0 }}>授权时间</Box>
|
||||||
|
|
@ -288,18 +277,6 @@ const AuthTypeModal = ({
|
||||||
value={selected}
|
value={selected}
|
||||||
change={(v: string) => setSelected(v as 'file' | 'code')}
|
change={(v: string) => setSelected(v as 'file' | 'code')}
|
||||||
/>
|
/>
|
||||||
<TextField
|
|
||||||
select
|
|
||||||
fullWidth
|
|
||||||
sx={{ mt: 2 }}
|
|
||||||
value={authVersion}
|
|
||||||
onChange={e =>
|
|
||||||
setAuthVersion(e.target.value as 'contributor' | 'enterprise')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuItem value='contributor'>联创版</MenuItem>
|
|
||||||
<MenuItem value='enterprise'>企业版</MenuItem>
|
|
||||||
</TextField>
|
|
||||||
{selected === 'code' && (
|
{selected === 'code' && (
|
||||||
<TextField
|
<TextField
|
||||||
sx={{ mt: 2 }}
|
sx={{ mt: 2 }}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,14 @@
|
||||||
import HelpCenter from '@/assets/json/help-center.json';
|
import HelpCenter from '@/assets/json/help-center.json';
|
||||||
import IconUpgrade from '@/assets/json/upgrade.json';
|
import IconUpgrade from '@/assets/json/upgrade.json';
|
||||||
import LottieIcon from '@/components/LottieIcon';
|
import LottieIcon from '@/components/LottieIcon';
|
||||||
import { EditionType } from '@/constant/enums';
|
|
||||||
import { useAppSelector } from '@/store';
|
|
||||||
import { Box, Stack, Tooltip } from '@mui/material';
|
import { Box, Stack, Tooltip } from '@mui/material';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import packageJson from '../../../package.json';
|
import packageJson from '../../../package.json';
|
||||||
import AuthTypeModal from './AuthTypeModal';
|
import AuthTypeModal from './AuthTypeModal';
|
||||||
import freeVersion from '@/assets/images/free-version.png';
|
import { useVersionInfo } from '@/hooks';
|
||||||
import enterpriseVersion from '@/assets/images/enterprise-version.png';
|
|
||||||
import contributorVersion from '@/assets/images/contributor-version.png';
|
|
||||||
|
|
||||||
const versionMap = {
|
|
||||||
0: freeVersion,
|
|
||||||
1: contributorVersion,
|
|
||||||
2: enterpriseVersion,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Version = () => {
|
const Version = () => {
|
||||||
const { license } = useAppSelector(state => state.config);
|
const versionInfo = useVersionInfo();
|
||||||
const curVersion = import.meta.env.VITE_APP_VERSION || packageJson.version;
|
const curVersion = import.meta.env.VITE_APP_VERSION || packageJson.version;
|
||||||
const [latestVersion, setLatestVersion] = useState<string | undefined>(
|
const [latestVersion, setLatestVersion] = useState<string | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
|
|
@ -57,11 +47,8 @@ const Version = () => {
|
||||||
>
|
>
|
||||||
<Stack direction={'row'} alignItems='center' gap={0.5}>
|
<Stack direction={'row'} alignItems='center' gap={0.5}>
|
||||||
<Box sx={{ width: 30, color: 'text.tertiary' }}>型号</Box>
|
<Box sx={{ width: 30, color: 'text.tertiary' }}>型号</Box>
|
||||||
<img
|
<img src={versionInfo.image} style={{ height: 13, marginTop: -1 }} />
|
||||||
src={versionMap[license.edition!]}
|
{versionInfo.label}
|
||||||
style={{ height: 13, marginTop: -1 }}
|
|
||||||
/>
|
|
||||||
{EditionType[license.edition as keyof typeof EditionType].text}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction={'row'} gap={0.5}>
|
<Stack direction={'row'} gap={0.5}>
|
||||||
<Box sx={{ width: 30, color: 'text.tertiary' }}>版本</Box>
|
<Box sx={{ width: 30, color: 'text.tertiary' }}>版本</Box>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ import { Modal, message } from '@ctzhian/ui';
|
||||||
import { useState, useMemo, useEffect } from 'react';
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||||
|
import { VersionCanUse } from '@/components/VersionMask';
|
||||||
import { ConstsUserKBPermission, V1KBUserInviteReq } from '@/request/types';
|
import { ConstsUserKBPermission, V1KBUserInviteReq } from '@/request/types';
|
||||||
import { ConstsLicenseEdition } from '@/request/pro/types';
|
import { ConstsLicenseEdition } from '@/request/pro/types';
|
||||||
|
|
||||||
|
|
@ -26,9 +27,13 @@ const VERSION_MAP = {
|
||||||
message: '开源版只支持 1 个管理员',
|
message: '开源版只支持 1 个管理员',
|
||||||
max: 1,
|
max: 1,
|
||||||
},
|
},
|
||||||
[ConstsLicenseEdition.LicenseEditionContributor]: {
|
[ConstsLicenseEdition.LicenseEditionProfession]: {
|
||||||
message: '联创版最多支持 3 个管理员',
|
message: '专业版最多支持 20 个管理员',
|
||||||
max: 3,
|
max: 20,
|
||||||
|
},
|
||||||
|
[ConstsLicenseEdition.LicenseEditionBusiness]: {
|
||||||
|
message: '商业版最多支持 50 个管理员',
|
||||||
|
max: 50,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -45,9 +50,6 @@ const MemberAdd = ({
|
||||||
const { kbList, license, refreshAdminRequest } = useAppSelector(
|
const { kbList, license, refreshAdminRequest } = useAppSelector(
|
||||||
state => state.config,
|
state => state.config,
|
||||||
);
|
);
|
||||||
const isEnterprise = useMemo(() => {
|
|
||||||
return license.edition === 2;
|
|
||||||
}, [license]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
|
|
@ -118,6 +120,10 @@ const MemberAdd = ({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isPro = useMemo(() => {
|
||||||
|
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||||
|
}, [license.edition]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -253,6 +259,14 @@ const MemberAdd = ({
|
||||||
fullWidth
|
fullWidth
|
||||||
displayEmpty
|
displayEmpty
|
||||||
sx={{ height: 52 }}
|
sx={{ height: 52 }}
|
||||||
|
MenuProps={{
|
||||||
|
sx: {
|
||||||
|
'.Mui-disabled': {
|
||||||
|
opacity: '1 !important',
|
||||||
|
color: 'text.disabled',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
renderValue={(value: V1KBUserInviteReq['perm']) => {
|
renderValue={(value: V1KBUserInviteReq['perm']) => {
|
||||||
return value ? (
|
return value ? (
|
||||||
PERM_MAP[value]
|
PERM_MAP[value]
|
||||||
|
|
@ -266,17 +280,25 @@ const MemberAdd = ({
|
||||||
>
|
>
|
||||||
完全控制
|
完全控制
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!isEnterprise}
|
|
||||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||||
|
disabled={!isPro}
|
||||||
>
|
>
|
||||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
文档管理{' '}
|
||||||
|
<VersionCanUse
|
||||||
|
permission={PROFESSION_VERSION_PERMISSION}
|
||||||
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!isEnterprise}
|
|
||||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||||
|
disabled={!isPro}
|
||||||
>
|
>
|
||||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
数据运营{' '}
|
||||||
|
<VersionCanUse
|
||||||
|
permission={PROFESSION_VERSION_PERMISSION}
|
||||||
|
/>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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: '其他',
|
3: '其他',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Free = 0;
|
|
||||||
export const Contributor = 1;
|
|
||||||
export const Enterprise = 2;
|
|
||||||
export const EditionType = {
|
|
||||||
[Free]: {
|
|
||||||
text: '开源版',
|
|
||||||
},
|
|
||||||
[Contributor]: {
|
|
||||||
text: '联创版',
|
|
||||||
},
|
|
||||||
[Enterprise]: {
|
|
||||||
text: '企业版',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DocWidth = {
|
export const DocWidth = {
|
||||||
full: {
|
full: {
|
||||||
label: '全屏',
|
label: '全屏',
|
||||||
|
|
|
||||||
|
|
@ -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 { useBindCaptcha } from './useBindCaptcha';
|
||||||
export { useCommitPendingInput } from './useCommitPendingInput';
|
export { useCommitPendingInput } from './useCommitPendingInput';
|
||||||
export { useURLSearchParams } from './useURLSearchParams';
|
export { useURLSearchParams } from './useURLSearchParams';
|
||||||
|
export {
|
||||||
|
useFeatureValue,
|
||||||
|
useFeatureValueSupported,
|
||||||
|
useVersionInfo,
|
||||||
|
} from './useVersionFeature';
|
||||||
|
|
|
||||||
|
|
@ -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 dayjs from 'dayjs';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import DocModal from './DocModal';
|
import DocModal from './DocModal';
|
||||||
|
import VersionMask from '@/components/VersionMask';
|
||||||
|
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||||
|
|
||||||
import { useURLSearchParams } from '@/hooks';
|
import { useURLSearchParams } from '@/hooks';
|
||||||
import {
|
import {
|
||||||
|
|
@ -46,7 +48,7 @@ const statusColorMap = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default function ContributionPage() {
|
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 [searchParams, setSearchParams] = useURLSearchParams();
|
||||||
const page = Number(searchParams.get('page') || '1');
|
const page = Number(searchParams.get('page') || '1');
|
||||||
const pageSize = Number(searchParams.get('page_size') || '20');
|
const pageSize = Number(searchParams.get('page_size') || '20');
|
||||||
|
|
@ -283,111 +285,114 @@ export default function ContributionPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (kb_id) getData();
|
if (kb_id && PROFESSION_VERSION_PERMISSION.includes(license.edition!))
|
||||||
|
getData();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [page, pageSize, nodeNameParam, authNameParam, kb_id]);
|
}, [page, pageSize, nodeNameParam, authNameParam, kb_id, license.edition]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Stack
|
<VersionMask permission={PROFESSION_VERSION_PERMISSION}>
|
||||||
direction='row'
|
<Stack
|
||||||
alignItems={'center'}
|
direction='row'
|
||||||
justifyContent={'space-between'}
|
alignItems={'center'}
|
||||||
sx={{ p: 2 }}
|
justifyContent={'space-between'}
|
||||||
>
|
sx={{ p: 2 }}
|
||||||
<StyledSearchRow direction='row' sx={{ p: 0, flex: 1 }}>
|
>
|
||||||
<TextField
|
<StyledSearchRow direction='row' sx={{ p: 0, flex: 1 }}>
|
||||||
fullWidth
|
<TextField
|
||||||
size='small'
|
fullWidth
|
||||||
label='文档'
|
size='small'
|
||||||
value={searchDoc}
|
label='文档'
|
||||||
onKeyUp={e => {
|
value={searchDoc}
|
||||||
if (e.key === 'Enter') {
|
onKeyUp={e => {
|
||||||
setSearchParams({ node_name: searchDoc || '', page: '1' });
|
if (e.key === 'Enter') {
|
||||||
}
|
setSearchParams({ node_name: searchDoc || '', page: '1' });
|
||||||
}}
|
}
|
||||||
onBlur={e => {
|
}}
|
||||||
setSearchParams({ node_name: e.target.value, page: '1' });
|
onBlur={e => {
|
||||||
}}
|
setSearchParams({ node_name: e.target.value, page: '1' });
|
||||||
onChange={e => setSearchDoc(e.target.value)}
|
}}
|
||||||
sx={{ width: 200 }}
|
onChange={e => setSearchDoc(e.target.value)}
|
||||||
/>
|
sx={{ width: 200 }}
|
||||||
<TextField
|
/>
|
||||||
fullWidth
|
<TextField
|
||||||
size='small'
|
fullWidth
|
||||||
label='用户'
|
size='small'
|
||||||
value={searchUser}
|
label='用户'
|
||||||
onKeyUp={e => {
|
value={searchUser}
|
||||||
if (e.key === 'Enter') {
|
onKeyUp={e => {
|
||||||
setSearchParams({ auth_name: searchUser || '', page: '1' });
|
if (e.key === 'Enter') {
|
||||||
}
|
setSearchParams({ auth_name: searchUser || '', page: '1' });
|
||||||
}}
|
}
|
||||||
onBlur={e => {
|
}}
|
||||||
setSearchParams({ auth_name: e.target.value, page: '1' });
|
onBlur={e => {
|
||||||
}}
|
setSearchParams({ auth_name: e.target.value, page: '1' });
|
||||||
onChange={e => setSearchUser(e.target.value)}
|
}}
|
||||||
sx={{ width: 200 }}
|
onChange={e => setSearchUser(e.target.value)}
|
||||||
/>
|
sx={{ width: 200 }}
|
||||||
</StyledSearchRow>
|
/>
|
||||||
</Stack>
|
</StyledSearchRow>
|
||||||
<Table
|
</Stack>
|
||||||
columns={columns}
|
<Table
|
||||||
dataSource={data}
|
columns={columns}
|
||||||
rowKey='id'
|
dataSource={data}
|
||||||
height='calc(100vh - 148px)'
|
rowKey='id'
|
||||||
size='small'
|
height='calc(100vh - 148px)'
|
||||||
sx={{
|
size='small'
|
||||||
overflow: 'hidden',
|
sx={{
|
||||||
...tableSx,
|
overflow: 'hidden',
|
||||||
'.MuiTableContainer-root': {
|
...tableSx,
|
||||||
height: 'calc(100vh - 148px - 70px)',
|
'.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,
|
|
||||||
},
|
},
|
||||||
},
|
}}
|
||||||
}}
|
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' ? (
|
{previewRow?.meta?.content_type === 'md' ? (
|
||||||
<MarkdownPreviewModal
|
<MarkdownPreviewModal
|
||||||
open={open}
|
open={open}
|
||||||
row={previewRow}
|
row={previewRow}
|
||||||
onClose={closeDialog}
|
onClose={closeDialog}
|
||||||
onAccept={handleAccept}
|
onAccept={handleAccept}
|
||||||
onReject={handleReject}
|
onReject={handleReject}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ContributePreviewModal
|
||||||
|
open={open}
|
||||||
|
row={previewRow}
|
||||||
|
onClose={closeDialog}
|
||||||
|
onAccept={handleAccept}
|
||||||
|
onReject={handleReject}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<DocModal
|
||||||
|
open={docModalOpen}
|
||||||
|
onClose={() => setDocModalOpen(false)}
|
||||||
|
onOk={handleDocModalOk}
|
||||||
/>
|
/>
|
||||||
) : (
|
</VersionMask>
|
||||||
<ContributePreviewModal
|
|
||||||
open={open}
|
|
||||||
row={previewRow}
|
|
||||||
onClose={closeDialog}
|
|
||||||
onAccept={handleAccept}
|
|
||||||
onReject={handleReject}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<DocModal
|
|
||||||
open={docModalOpen}
|
|
||||||
onClose={() => setDocModalOpen(false)}
|
|
||||||
onOk={handleDocModalOk}
|
|
||||||
/>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import { ChatConversationPair } from '@/api';
|
||||||
import { getApiV1ConversationDetail } from '@/request/Conversation';
|
import { getApiV1ConversationDetail } from '@/request/Conversation';
|
||||||
import { DomainConversationDetailResp } from '@/request/types';
|
import { DomainConversationDetailResp } from '@/request/types';
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import MarkDown from '@/components/MarkDown';
|
import MarkDown from '@/components/MarkDown';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
|
|
@ -13,10 +13,169 @@ import {
|
||||||
Box,
|
Box,
|
||||||
Stack,
|
Stack,
|
||||||
useTheme,
|
useTheme,
|
||||||
|
styled,
|
||||||
|
alpha,
|
||||||
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Ellipsis, Icon, Modal } from '@ctzhian/ui';
|
import { Ellipsis, Icon, Modal } from '@ctzhian/ui';
|
||||||
import { useEffect, useState } from 'react';
|
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 = ({
|
const Detail = ({
|
||||||
id,
|
id,
|
||||||
open,
|
open,
|
||||||
|
|
@ -55,7 +214,11 @@ const Detail = ({
|
||||||
};
|
};
|
||||||
} else if (message.role === 'assistant') {
|
} else if (message.role === 'assistant') {
|
||||||
if (currentPair.user) {
|
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;
|
currentPair.created_at = message.created_at;
|
||||||
// @ts-expect-error 类型不兼容
|
// @ts-expect-error 类型不兼容
|
||||||
currentPair.info = message.info;
|
currentPair.info = message.info;
|
||||||
|
|
@ -167,26 +330,43 @@ const Detail = ({
|
||||||
<Stack gap={2}>
|
<Stack gap={2}>
|
||||||
{conversations &&
|
{conversations &&
|
||||||
conversations.map((item, index) => (
|
conversations.map((item, index) => (
|
||||||
<Box key={index}>
|
<StyledConversationItem key={index}>
|
||||||
<Accordion defaultExpanded={true}>
|
{/* 用户问题气泡 - 右对齐 */}
|
||||||
<AccordionSummary
|
<StyledUserBubble>{item.user}</StyledUserBubble>
|
||||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 24 }} />}
|
|
||||||
sx={{
|
{/* AI回答气泡 - 左对齐 */}
|
||||||
userSelect: 'text',
|
<StyledAiBubble>
|
||||||
backgroundColor: 'background.paper3',
|
{/* 思考过程 */}
|
||||||
fontSize: '18px',
|
{!!item.thinking_content && (
|
||||||
fontWeight: 'bold',
|
<StyledThinkingAccordion defaultExpanded>
|
||||||
}}
|
<StyledThinkingAccordionSummary
|
||||||
>
|
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||||
{item.user}
|
>
|
||||||
</AccordionSummary>
|
<Stack direction='row' alignItems='center' gap={1}>
|
||||||
<AccordionDetails>
|
<Typography
|
||||||
<MarkDown
|
variant='body2'
|
||||||
content={item.assistant || '未查询到回答内容'}
|
sx={theme => ({
|
||||||
/>
|
fontSize: 12,
|
||||||
</AccordionDetails>
|
color: alpha(theme.palette.text.primary, 0.5),
|
||||||
</Accordion>
|
})}
|
||||||
</Box>
|
>
|
||||||
|
已思考
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</StyledThinkingAccordionSummary>
|
||||||
|
|
||||||
|
<StyledThinkingAccordionDetails>
|
||||||
|
<MarkDown content={item.thinking_content || ''} />
|
||||||
|
</StyledThinkingAccordionDetails>
|
||||||
|
</StyledThinkingAccordion>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* AI回答内容 */}
|
||||||
|
<StyledAiBubbleContent>
|
||||||
|
<MarkDown content={item.assistant} />
|
||||||
|
</StyledAiBubbleContent>
|
||||||
|
</StyledAiBubble>
|
||||||
|
</StyledConversationItem>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ import {
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||||
|
import { VersionCanUse } from '@/components/VersionMask';
|
||||||
|
|
||||||
interface DocPropertiesModalProps {
|
interface DocPropertiesModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -40,8 +42,6 @@ interface DocPropertiesModalProps {
|
||||||
data: DomainNodeListItemResp[];
|
data: DomainNodeListItemResp[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const tips = '(企业版可用)';
|
|
||||||
|
|
||||||
const StyledText = styled('div')(({ theme }) => ({
|
const StyledText = styled('div')(({ theme }) => ({
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
|
@ -53,7 +53,12 @@ const PER_OPTIONS = [
|
||||||
value: ConstsNodeAccessPerm.NodeAccessPermOpen,
|
value: ConstsNodeAccessPerm.NodeAccessPermOpen,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '部分开放',
|
label: (
|
||||||
|
<Stack direction={'row'} alignItems={'center'}>
|
||||||
|
<span>部分开放</span>
|
||||||
|
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
value: ConstsNodeAccessPerm.NodeAccessPermPartial,
|
value: ConstsNodeAccessPerm.NodeAccessPermPartial,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -128,13 +133,13 @@ const DocPropertiesModal = ({
|
||||||
visitable: values.visitable as ConstsNodeAccessPerm,
|
visitable: values.visitable as ConstsNodeAccessPerm,
|
||||||
visible: values.visible as ConstsNodeAccessPerm,
|
visible: values.visible as ConstsNodeAccessPerm,
|
||||||
},
|
},
|
||||||
answerable_groups: isEnterprise
|
answerable_groups: isBusiness
|
||||||
? values.answerable_groups.map(item => item.id!)
|
? values.answerable_groups.map(item => item.id!)
|
||||||
: undefined,
|
: undefined,
|
||||||
visitable_groups: isEnterprise
|
visitable_groups: isBusiness
|
||||||
? values.visitable_groups.map(item => item.id!)
|
? values.visitable_groups.map(item => item.id!)
|
||||||
: undefined,
|
: undefined,
|
||||||
visible_groups: isEnterprise
|
visible_groups: isBusiness
|
||||||
? values.visible_groups.map(item => item.id!)
|
? values.visible_groups.map(item => item.id!)
|
||||||
: undefined,
|
: undefined,
|
||||||
}),
|
}),
|
||||||
|
|
@ -153,15 +158,15 @@ const DocPropertiesModal = ({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const isEnterprise = useMemo(() => {
|
const isBusiness = useMemo(() => {
|
||||||
return license.edition === 2;
|
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||||
}, [license]);
|
}, [license]);
|
||||||
|
|
||||||
const tree = filterEmptyFolders(convertToTree(data));
|
const tree = filterEmptyFolders(convertToTree(data));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && data) {
|
if (open && data) {
|
||||||
if (isEnterprise) {
|
if (isBusiness) {
|
||||||
getApiProV1AuthGroupList({
|
getApiProV1AuthGroupList({
|
||||||
kb_id: kb_id!,
|
kb_id: kb_id!,
|
||||||
page: 1,
|
page: 1,
|
||||||
|
|
@ -206,7 +211,7 @@ const DocPropertiesModal = ({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [open, data, isEnterprise]);
|
}, [open, data, isBusiness]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
|
@ -302,22 +307,15 @@ const DocPropertiesModal = ({
|
||||||
name='answerable'
|
name='answerable'
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<RadioGroup row {...field}>
|
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||||
{PER_OPTIONS.map(option => (
|
{PER_OPTIONS.map(option => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
control={<Radio size='small' />}
|
control={<Radio size='small' />}
|
||||||
label={
|
label={option.label}
|
||||||
option.label +
|
|
||||||
(!isEnterprise &&
|
|
||||||
option.value ===
|
|
||||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
|
||||||
? tips
|
|
||||||
: '')
|
|
||||||
}
|
|
||||||
disabled={
|
disabled={
|
||||||
!isEnterprise &&
|
!isBusiness &&
|
||||||
option.value ===
|
option.value ===
|
||||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||||
}
|
}
|
||||||
|
|
@ -359,22 +357,15 @@ const DocPropertiesModal = ({
|
||||||
name='visitable'
|
name='visitable'
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<RadioGroup row {...field}>
|
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||||
{PER_OPTIONS.map(option => (
|
{PER_OPTIONS.map(option => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
control={<Radio size='small' />}
|
control={<Radio size='small' />}
|
||||||
label={
|
label={option.label}
|
||||||
option.label +
|
|
||||||
(!isEnterprise &&
|
|
||||||
option.value ===
|
|
||||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
|
||||||
? tips
|
|
||||||
: '')
|
|
||||||
}
|
|
||||||
disabled={
|
disabled={
|
||||||
!isEnterprise &&
|
!isBusiness &&
|
||||||
option.value ===
|
option.value ===
|
||||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||||
}
|
}
|
||||||
|
|
@ -416,22 +407,15 @@ const DocPropertiesModal = ({
|
||||||
name='visible'
|
name='visible'
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<RadioGroup row {...field}>
|
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||||
{PER_OPTIONS.map(option => (
|
{PER_OPTIONS.map(option => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
control={<Radio size='small' />}
|
control={<Radio size='small' />}
|
||||||
label={
|
label={option.label}
|
||||||
option.label +
|
|
||||||
(!isEnterprise &&
|
|
||||||
option.value ===
|
|
||||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
|
||||||
? tips
|
|
||||||
: '')
|
|
||||||
}
|
|
||||||
disabled={
|
disabled={
|
||||||
!isEnterprise &&
|
!isBusiness &&
|
||||||
option.value ===
|
option.value ===
|
||||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import { useNavigate, useOutletContext } from 'react-router-dom';
|
||||||
import { WrapContext } from '..';
|
import { WrapContext } from '..';
|
||||||
import DocAddByCustomText from '../../component/DocAddByCustomText';
|
import DocAddByCustomText from '../../component/DocAddByCustomText';
|
||||||
import DocDelete from '../../component/DocDelete';
|
import DocDelete from '../../component/DocDelete';
|
||||||
|
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||||
|
import { VersionCanUse } from '@/components/VersionMask';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
edit: boolean;
|
edit: boolean;
|
||||||
|
|
@ -52,8 +54,8 @@ const Header = ({
|
||||||
|
|
||||||
const [showSaveTip, setShowSaveTip] = useState(false);
|
const [showSaveTip, setShowSaveTip] = useState(false);
|
||||||
|
|
||||||
const isEnterprise = useMemo(() => {
|
const isBusiness = useMemo(() => {
|
||||||
return license.edition === 2;
|
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||||
}, [license]);
|
}, [license]);
|
||||||
|
|
||||||
const handlePublish = useCallback(() => {
|
const handlePublish = useCallback(() => {
|
||||||
|
|
@ -309,6 +311,7 @@ const Header = ({
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
key: 'copy',
|
key: 'copy',
|
||||||
|
textSx: { flex: 1 },
|
||||||
label: <StyledMenuSelect>复制</StyledMenuSelect>,
|
label: <StyledMenuSelect>复制</StyledMenuSelect>,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (kb_id) {
|
if (kb_id) {
|
||||||
|
|
@ -328,26 +331,22 @@ const Header = ({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'version',
|
key: 'version',
|
||||||
|
textSx: { flex: 1 },
|
||||||
label: (
|
label: (
|
||||||
<StyledMenuSelect disabled={!isEnterprise}>
|
<StyledMenuSelect disabled={!isBusiness}>
|
||||||
历史版本{' '}
|
历史版本
|
||||||
{!isEnterprise && (
|
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||||
<Tooltip title='企业版可用' placement='top' arrow>
|
|
||||||
<InfoIcon
|
|
||||||
sx={{ color: 'text.secondary', fontSize: 14 }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</StyledMenuSelect>
|
</StyledMenuSelect>
|
||||||
),
|
),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (isEnterprise) {
|
if (isBusiness) {
|
||||||
navigate(`/doc/editor/history/${detail.id}`);
|
navigate(`/doc/editor/history/${detail.id}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'rename',
|
key: 'rename',
|
||||||
|
textSx: { flex: 1 },
|
||||||
label: <StyledMenuSelect>重命名</StyledMenuSelect>,
|
label: <StyledMenuSelect>重命名</StyledMenuSelect>,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setRenameOpen(true);
|
setRenameOpen(true);
|
||||||
|
|
@ -355,6 +354,7 @@ const Header = ({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
|
textSx: { flex: 1 },
|
||||||
label: <StyledMenuSelect>删除</StyledMenuSelect>,
|
label: <StyledMenuSelect>删除</StyledMenuSelect>,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setDelOpen(true);
|
setDelOpen(true);
|
||||||
|
|
@ -566,7 +566,7 @@ const StyledMenuSelect = styled('div')<{ disabled?: boolean }>(
|
||||||
padding: theme.spacing(0, 2),
|
padding: theme.spacing(0, 2),
|
||||||
lineHeight: '40px',
|
lineHeight: '40px',
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 106,
|
minWidth: 106,
|
||||||
borderRadius: '5px',
|
borderRadius: '5px',
|
||||||
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
|
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
|
||||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import Header from './Header';
|
||||||
import Summary from './Summary';
|
import Summary from './Summary';
|
||||||
import Toc from './Toc';
|
import Toc from './Toc';
|
||||||
import Toolbar from './Toolbar';
|
import Toolbar from './Toolbar';
|
||||||
|
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||||
|
|
||||||
interface WrapProps {
|
interface WrapProps {
|
||||||
detail: V1NodeDetailResp;
|
detail: V1NodeDetailResp;
|
||||||
|
|
@ -72,8 +73,8 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
||||||
emoji: defaultDetail.meta?.emoji || '',
|
emoji: defaultDetail.meta?.emoji || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const isEnterprise = useMemo(() => {
|
const isBusiness = useMemo(() => {
|
||||||
return license.edition === 2;
|
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||||
}, [license]);
|
}, [license]);
|
||||||
|
|
||||||
const debouncedUpdateSummary = useCallback(
|
const debouncedUpdateSummary = useCallback(
|
||||||
|
|
@ -383,7 +384,7 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
||||||
</Stack>
|
</Stack>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Tooltip arrow title={isEnterprise ? '查看历史版本' : ''}>
|
<Tooltip arrow title={isBusiness ? '查看历史版本' : ''}>
|
||||||
<Stack
|
<Stack
|
||||||
direction={'row'}
|
direction={'row'}
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
|
|
@ -391,13 +392,13 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: 'text.tertiary',
|
color: 'text.tertiary',
|
||||||
cursor: isEnterprise ? 'pointer' : 'text',
|
cursor: isBusiness ? 'pointer' : 'text',
|
||||||
':hover': {
|
':hover': {
|
||||||
color: isEnterprise ? 'primary.main' : 'text.tertiary',
|
color: isBusiness ? 'primary.main' : 'text.tertiary',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isEnterprise) {
|
if (isBusiness) {
|
||||||
navigate(`/doc/editor/history/${defaultDetail.id}`);
|
navigate(`/doc/editor/history/${defaultDetail.id}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
IconButton,
|
IconButton,
|
||||||
Stack,
|
Stack,
|
||||||
useTheme,
|
useTheme,
|
||||||
|
ButtonBase,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import VersionPublish from '../release/components/VersionPublish';
|
import VersionPublish from '../release/components/VersionPublish';
|
||||||
|
|
@ -418,15 +419,19 @@ const Content = () => {
|
||||||
>
|
>
|
||||||
{publish.unpublished} 个 文档/文件夹未发布,
|
{publish.unpublished} 个 文档/文件夹未发布,
|
||||||
</Box>
|
</Box>
|
||||||
<Button
|
<ButtonBase
|
||||||
size='small'
|
disableRipple
|
||||||
sx={{ minWidth: 0, p: 0, fontSize: 12 }}
|
sx={{
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 400,
|
||||||
|
color: 'primary.main',
|
||||||
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPublishOpen(true);
|
setPublishOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
去发布
|
去发布
|
||||||
</Button>
|
</ButtonBase>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{ragReStartCount > 0 && (
|
{ragReStartCount > 0 && (
|
||||||
|
|
@ -441,15 +446,19 @@ const Content = () => {
|
||||||
>
|
>
|
||||||
{ragReStartCount} 个文档未学习,
|
{ragReStartCount} 个文档未学习,
|
||||||
</Box>
|
</Box>
|
||||||
<Button
|
<ButtonBase
|
||||||
size='small'
|
disableRipple
|
||||||
sx={{ minWidth: 0, p: 0, fontSize: 12 }}
|
sx={{
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 400,
|
||||||
|
color: 'primary.main',
|
||||||
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setRagOpen(true);
|
setRagOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
去学习
|
去学习
|
||||||
</Button>
|
</ButtonBase>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
ButtonBase,
|
ButtonBase,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Ellipsis, Table, Modal, Icon, message } from '@ctzhian/ui';
|
import { Ellipsis, Table, Modal, Icon, message } from '@ctzhian/ui';
|
||||||
|
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useEffect, useState, useMemo } from 'react';
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
|
|
||||||
|
|
@ -162,8 +163,8 @@ const Comments = ({
|
||||||
useState<DomainWebAppCommentSettings | null>(null);
|
useState<DomainWebAppCommentSettings | null>(null);
|
||||||
|
|
||||||
const isEnableReview = useMemo(() => {
|
const isEnableReview = useMemo(() => {
|
||||||
return !!(license.edition === 1 || license.edition === 2);
|
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||||
}, [license]);
|
}, [license.edition]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowCommentsFilter(isEnableReview);
|
setShowCommentsFilter(isEnableReview);
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,18 @@ import { getApiV1ConversationMessageDetail } from '@/request';
|
||||||
import MarkDown from '@/components/MarkDown';
|
import MarkDown from '@/components/MarkDown';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import {
|
import { Box, Stack, Typography, alpha } from '@mui/material';
|
||||||
Accordion,
|
import { Ellipsis, Modal } from '@ctzhian/ui';
|
||||||
AccordionDetails,
|
|
||||||
AccordionSummary,
|
|
||||||
Box,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Ellipsis, Icon, Modal } from '@ctzhian/ui';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
StyledConversationItem,
|
||||||
|
StyledUserBubble,
|
||||||
|
StyledAiBubble,
|
||||||
|
StyledThinkingAccordion,
|
||||||
|
StyledThinkingAccordionSummary,
|
||||||
|
StyledThinkingAccordionDetails,
|
||||||
|
StyledAiBubbleContent,
|
||||||
|
} from '../conversation/Detail';
|
||||||
|
|
||||||
const Detail = ({
|
const Detail = ({
|
||||||
id,
|
id,
|
||||||
|
|
@ -36,6 +40,7 @@ const Detail = ({
|
||||||
user: data.question,
|
user: data.question,
|
||||||
assistant: res.content!,
|
assistant: res.content!,
|
||||||
created_at: res.created_at!,
|
created_at: res.created_at!,
|
||||||
|
thinking_content: '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -62,24 +67,43 @@ const Detail = ({
|
||||||
>
|
>
|
||||||
<Box sx={{ fontSize: 14 }}>
|
<Box sx={{ fontSize: 14 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Accordion defaultExpanded={true}>
|
<StyledConversationItem>
|
||||||
<AccordionSummary
|
{/* 用户问题气泡 - 右对齐 */}
|
||||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 24 }} />}
|
<StyledUserBubble>{conversations?.user}</StyledUserBubble>
|
||||||
sx={{
|
|
||||||
userSelect: 'text',
|
{/* AI回答气泡 - 左对齐 */}
|
||||||
backgroundColor: 'background.paper3',
|
<StyledAiBubble>
|
||||||
fontSize: '18px',
|
{/* 思考过程 */}
|
||||||
fontWeight: 'bold',
|
{!!conversations?.thinking_content && (
|
||||||
}}
|
<StyledThinkingAccordion defaultExpanded>
|
||||||
>
|
<StyledThinkingAccordionSummary
|
||||||
{conversations?.user}
|
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||||
</AccordionSummary>
|
>
|
||||||
<AccordionDetails>
|
<Stack direction='row' alignItems='center' gap={1}>
|
||||||
<MarkDown
|
<Typography
|
||||||
content={conversations?.assistant || '未查询到回答内容'}
|
variant='body2'
|
||||||
/>
|
sx={theme => ({
|
||||||
</AccordionDetails>
|
fontSize: 12,
|
||||||
</Accordion>
|
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>
|
||||||
</Box>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import dayjs from 'dayjs';
|
||||||
import { ColumnType } from '@ctzhian/ui/dist/Table';
|
import { ColumnType } from '@ctzhian/ui/dist/Table';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
import { VersionCanUse } from '@/components/VersionMask';
|
||||||
|
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||||
|
|
||||||
interface AddRoleProps {
|
interface AddRoleProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -23,7 +25,8 @@ interface AddRoleProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddRole = ({ open, onCancel, onOk, selectedIds }: 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 [list, setList] = useState<V1UserListItemResp[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string>('');
|
const [selectedRowKeys, setSelectedRowKeys] = useState<string>('');
|
||||||
|
|
@ -31,10 +34,6 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
||||||
ConstsUserKBPermission.UserKBPermissionFullControl,
|
ConstsUserKBPermission.UserKBPermissionFullControl,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEnterprise = useMemo(() => {
|
|
||||||
return license.edition === 2;
|
|
||||||
}, [license]);
|
|
||||||
|
|
||||||
const columns: ColumnType<V1UserListItemResp>[] = [
|
const columns: ColumnType<V1UserListItemResp>[] = [
|
||||||
{
|
{
|
||||||
title: '',
|
title: '',
|
||||||
|
|
@ -119,6 +118,10 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
const isPro = useMemo(() => {
|
||||||
|
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||||
|
}, [license.edition]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title='添加 Wiki 站管理员'
|
title='添加 Wiki 站管理员'
|
||||||
|
|
@ -209,22 +212,33 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ height: 52 }}
|
sx={{ height: 52 }}
|
||||||
value={perm}
|
value={perm}
|
||||||
|
MenuProps={{
|
||||||
|
sx: {
|
||||||
|
'.Mui-disabled': {
|
||||||
|
opacity: '1 !important',
|
||||||
|
color: 'text.disabled',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
onChange={e => setPerm(e.target.value as V1KBUserInviteReq['perm'])}
|
onChange={e => setPerm(e.target.value as V1KBUserInviteReq['perm'])}
|
||||||
>
|
>
|
||||||
<MenuItem value={ConstsUserKBPermission.UserKBPermissionFullControl}>
|
<MenuItem value={ConstsUserKBPermission.UserKBPermissionFullControl}>
|
||||||
完全控制
|
完全控制
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!isEnterprise}
|
|
||||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||||
|
disabled={!isPro}
|
||||||
>
|
>
|
||||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
文档管理{' '}
|
||||||
|
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!isEnterprise}
|
|
||||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||||
|
disabled={!isPro}
|
||||||
>
|
>
|
||||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
数据运营{' '}
|
||||||
|
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { getApiProV1Prompt, postApiProV1Prompt } from '@/request/pro/Prompt';
|
import { getApiProV1Prompt, postApiProV1Prompt } from '@/request/pro/Prompt';
|
||||||
import { DomainKnowledgeBaseDetail } from '@/request/types';
|
import { DomainKnowledgeBaseDetail } from '@/request/types';
|
||||||
|
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import { message } from '@ctzhian/ui';
|
import { message } from '@ctzhian/ui';
|
||||||
import { Box, Slider, TextField } from '@mui/material';
|
import { Box, Slider, TextField } from '@mui/material';
|
||||||
|
|
@ -33,11 +34,12 @@ const CardAI = ({ kb }: CardAIProps) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPro = useMemo(() => {
|
const isPro = useMemo(() => {
|
||||||
return license.edition === 1 || license.edition === 2;
|
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||||
}, [license]);
|
}, [license]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!kb.id || !isPro) return;
|
if (!kb.id || !PROFESSION_VERSION_PERMISSION.includes(license.edition!))
|
||||||
|
return;
|
||||||
getApiProV1Prompt({ kb_id: kb.id! }).then(res => {
|
getApiProV1Prompt({ kb_id: kb.id! }).then(res => {
|
||||||
setValue('content', res.content || '');
|
setValue('content', res.content || '');
|
||||||
});
|
});
|
||||||
|
|
@ -54,7 +56,7 @@ const CardAI = ({ kb }: CardAIProps) => {
|
||||||
<SettingCardItem title='智能问答' isEdit={isEdit} onSubmit={onSubmit}>
|
<SettingCardItem title='智能问答' isEdit={isEdit} onSubmit={onSubmit}>
|
||||||
<FormItem
|
<FormItem
|
||||||
vertical
|
vertical
|
||||||
tooltip={!isPro && '联创版和企业版可用'}
|
permission={PROFESSION_VERSION_PERMISSION}
|
||||||
extra={
|
extra={
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ import { ColumnType } from '@ctzhian/ui/dist/Table';
|
||||||
import { useEffect, useMemo, useState, useRef } from 'react';
|
import { useEffect, useMemo, useState, useRef } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||||
|
import { VersionCanUse } from '@/components/VersionMask';
|
||||||
import { SettingCardItem, FormItem } from './Common';
|
import { SettingCardItem, FormItem } from './Common';
|
||||||
|
|
||||||
interface CardAuthProps {
|
interface CardAuthProps {
|
||||||
|
|
@ -114,7 +116,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
||||||
}),
|
}),
|
||||||
value.enabled === '2' &&
|
value.enabled === '2' &&
|
||||||
source_type !== EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword
|
source_type !== EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword
|
||||||
? isPro
|
? isBusiness
|
||||||
? postApiProV1AuthSet({
|
? postApiProV1AuthSet({
|
||||||
kb_id,
|
kb_id,
|
||||||
source_type: value.source_type as ConstsSourceType,
|
source_type: value.source_type as ConstsSourceType,
|
||||||
|
|
@ -157,25 +159,18 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPro = useMemo(() => {
|
const isBusiness = useMemo(() => {
|
||||||
return license.edition === 1 || license.edition === 2;
|
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||||
}, [license]);
|
|
||||||
|
|
||||||
const isEnterprise = useMemo(() => {
|
|
||||||
return license.edition === 2;
|
|
||||||
}, [license]);
|
}, [license]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const source_type = isPro
|
const source_type = isBusiness
|
||||||
? kb.access_settings?.source_type ||
|
? kb.access_settings?.source_type ||
|
||||||
EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword
|
EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword
|
||||||
: kb.access_settings?.source_type ===
|
: EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword;
|
||||||
EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub
|
|
||||||
? EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub
|
|
||||||
: EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword;
|
|
||||||
setValue('source_type', source_type);
|
setValue('source_type', source_type);
|
||||||
sourceTypeRef.current = source_type;
|
sourceTypeRef.current = source_type;
|
||||||
}, [kb, isPro]);
|
}, [kb, isBusiness]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (kb.access_settings?.simple_auth) {
|
if (kb.access_settings?.simple_auth) {
|
||||||
|
|
@ -191,7 +186,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
||||||
}, [kb]);
|
}, [kb]);
|
||||||
|
|
||||||
const getAuth = () => {
|
const getAuth = () => {
|
||||||
if (isPro) {
|
if (isBusiness) {
|
||||||
getApiProV1AuthGet({
|
getApiProV1AuthGet({
|
||||||
kb_id,
|
kb_id,
|
||||||
source_type: source_type as ConstsSourceType,
|
source_type: source_type as ConstsSourceType,
|
||||||
|
|
@ -236,7 +231,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!kb_id || enabled !== '2') return;
|
if (!kb_id || enabled !== '2') return;
|
||||||
getAuth();
|
getAuth();
|
||||||
}, [kb_id, isPro, source_type, enabled]);
|
}, [kb_id, isBusiness, source_type, enabled]);
|
||||||
|
|
||||||
const columns: ColumnType<GithubComChaitinPandaWikiProApiAuthV1AuthItem>[] = [
|
const columns: ColumnType<GithubComChaitinPandaWikiProApiAuthV1AuthItem>[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -875,8 +870,18 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
||||||
field.onChange(e.target.value);
|
field.onChange(e.target.value);
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
}}
|
}}
|
||||||
|
MenuProps={{
|
||||||
|
sx: {
|
||||||
|
'.Mui-disabled': {
|
||||||
|
opacity: '1 !important',
|
||||||
|
color: 'text.disabled',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
sx={{ height: 52 }}
|
sx={{
|
||||||
|
height: 52,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword}
|
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword}
|
||||||
|
|
@ -885,44 +890,52 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeDingTalk}
|
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeDingTalk}
|
||||||
disabled={!isPro}
|
disabled={!isBusiness}
|
||||||
>
|
>
|
||||||
钉钉登录 {isPro ? '' : tips}
|
钉钉登录{' '}
|
||||||
|
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeFeishu}
|
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeFeishu}
|
||||||
disabled={!isPro}
|
disabled={!isBusiness}
|
||||||
>
|
>
|
||||||
飞书登录 {isPro ? '' : tips}
|
飞书登录{' '}
|
||||||
|
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeWeCom}
|
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeWeCom}
|
||||||
disabled={!isPro}
|
disabled={!isBusiness}
|
||||||
>
|
>
|
||||||
企业微信登录 {isPro ? '' : tips}
|
企业微信登录{' '}
|
||||||
|
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeOAuth}
|
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeOAuth}
|
||||||
disabled={!isPro}
|
disabled={!isBusiness}
|
||||||
>
|
>
|
||||||
OAuth 登录 {isPro ? '' : tips}
|
OAuth 登录{' '}
|
||||||
|
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeCAS}
|
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeCAS}
|
||||||
disabled={!isPro}
|
disabled={!isBusiness}
|
||||||
>
|
>
|
||||||
CAS 登录 {isPro ? '' : tips}
|
CAS 登录{' '}
|
||||||
|
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeLDAP}
|
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeLDAP}
|
||||||
disabled={!isPro}
|
disabled={!isBusiness}
|
||||||
>
|
>
|
||||||
LDAP 登录 {isPro ? '' : tips}
|
LDAP 登录{' '}
|
||||||
|
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub}
|
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub}
|
||||||
|
disabled={!isBusiness}
|
||||||
>
|
>
|
||||||
GitHub 登录
|
GitHub 登录{' '}
|
||||||
|
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import {
|
||||||
DomainKnowledgeBaseDetail,
|
DomainKnowledgeBaseDetail,
|
||||||
} from '@/request/types';
|
} from '@/request/types';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import InfoIcon from '@mui/icons-material/Info';
|
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Chip,
|
Chip,
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
styled,
|
styled,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
||||||
|
|
@ -37,7 +36,7 @@ const DocumentComments = ({
|
||||||
data: DomainAppDetailResp;
|
data: DomainAppDetailResp;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { license, kb_id } = useAppSelector(state => state.config);
|
const { kb_id } = useAppSelector(state => state.config);
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
const { control, handleSubmit, setValue } = useForm({
|
const { control, handleSubmit, setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|
@ -57,8 +56,6 @@ const DocumentComments = ({
|
||||||
);
|
);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const isPro = license.edition === 1 || license.edition === 2;
|
|
||||||
|
|
||||||
const onSubmit = handleSubmit(formData => {
|
const onSubmit = handleSubmit(formData => {
|
||||||
putApiV1App(
|
putApiV1App(
|
||||||
{ id: data.id! },
|
{ id: data.id! },
|
||||||
|
|
@ -108,7 +105,7 @@ const DocumentComments = ({
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label='评论审核' tooltip={!isPro && '联创版和企业版可用'}>
|
<FormItem label='评论审核' permission={PROFESSION_VERSION_PERMISSION}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='moderation_enable'
|
name='moderation_enable'
|
||||||
|
|
@ -116,7 +113,6 @@ const DocumentComments = ({
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
row
|
row
|
||||||
{...field}
|
{...field}
|
||||||
value={isPro ? field.value : undefined}
|
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
field.onChange(+e.target.value as 1 | 0);
|
field.onChange(+e.target.value as 1 | 0);
|
||||||
|
|
@ -124,12 +120,12 @@ const DocumentComments = ({
|
||||||
>
|
>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={1}
|
value={1}
|
||||||
control={<Radio size='small' disabled={!isPro} />}
|
control={<Radio size='small' />}
|
||||||
label={<StyledRadioLabel>启用</StyledRadioLabel>}
|
label={<StyledRadioLabel>启用</StyledRadioLabel>}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={0}
|
value={0}
|
||||||
control={<Radio size='small' disabled={!isPro} />}
|
control={<Radio size='small' />}
|
||||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
@ -150,7 +146,7 @@ const AIQuestion = ({
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
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({
|
const { control, handleSubmit, setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
is_enabled: true,
|
is_enabled: true,
|
||||||
|
|
@ -159,7 +155,6 @@ const AIQuestion = ({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const isEnterprise = license.edition === 2;
|
|
||||||
|
|
||||||
const onSubmit = handleSubmit(formData => {
|
const onSubmit = handleSubmit(formData => {
|
||||||
putApiV1App(
|
putApiV1App(
|
||||||
|
|
@ -273,7 +268,7 @@ const AIQuestion = ({
|
||||||
)}
|
)}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem label='免责声明' tooltip={!isEnterprise && '企业版可用'}>
|
<FormItem label='免责声明' permission={PROFESSION_VERSION_PERMISSION}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='disclaimer'
|
name='disclaimer'
|
||||||
|
|
@ -282,7 +277,6 @@ const AIQuestion = ({
|
||||||
{...field}
|
{...field}
|
||||||
fullWidth
|
fullWidth
|
||||||
value={field.value || ''}
|
value={field.value || ''}
|
||||||
disabled={!isEnterprise}
|
|
||||||
placeholder='请输入免责声明'
|
placeholder='请输入免责声明'
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
|
|
@ -304,7 +298,7 @@ const DocumentContribution = ({
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
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({
|
const { control, handleSubmit, setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
is_enable: false,
|
is_enable: false,
|
||||||
|
|
@ -330,7 +324,6 @@ const DocumentContribution = ({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPro = license.edition === 1 || license.edition === 2;
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(
|
setValue(
|
||||||
'is_enable',
|
'is_enable',
|
||||||
|
|
@ -340,21 +333,8 @@ const DocumentContribution = ({
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingCardItem
|
<SettingCardItem title='文档贡献' isEdit={isEdit} onSubmit={onSubmit}>
|
||||||
title={
|
<FormItem label='文档贡献' permission={PROFESSION_VERSION_PERMISSION}>
|
||||||
<>
|
|
||||||
文档贡献
|
|
||||||
{!isPro && (
|
|
||||||
<Tooltip title='联创版和企业版可用' placement='top' arrow>
|
|
||||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14 }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
isEdit={isEdit}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
<FormItem label='文档贡献'>
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='is_enable'
|
name='is_enable'
|
||||||
|
|
@ -362,7 +342,7 @@ const DocumentContribution = ({
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
row
|
row
|
||||||
{...field}
|
{...field}
|
||||||
value={isPro ? field.value : undefined}
|
value={field.value}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setIsEdit(true);
|
setIsEdit(true);
|
||||||
field.onChange(e.target.value === 'true');
|
field.onChange(e.target.value === 'true');
|
||||||
|
|
@ -370,12 +350,12 @@ const DocumentContribution = ({
|
||||||
>
|
>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={true}
|
value={true}
|
||||||
control={<Radio size='small' disabled={!isPro} />}
|
control={<Radio size='small' />}
|
||||||
label={<StyledRadioLabel>启用</StyledRadioLabel>}
|
label={<StyledRadioLabel>启用</StyledRadioLabel>}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={false}
|
value={false}
|
||||||
control={<Radio size='small' disabled={!isPro} />}
|
control={<Radio size='small' />}
|
||||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import AddRole from './AddRole';
|
import AddRole from './AddRole';
|
||||||
import { Form, FormItem, SettingCardItem } from './Common';
|
import { Form, FormItem, SettingCardItem } from './Common';
|
||||||
|
import {
|
||||||
|
PROFESSION_VERSION_PERMISSION,
|
||||||
|
BUSINESS_VERSION_PERMISSION,
|
||||||
|
} from '@/constant/version';
|
||||||
|
|
||||||
type ApiTokenPermission =
|
type ApiTokenPermission =
|
||||||
GithubComChaitinPandaWikiProApiTokenV1CreateAPITokenReq['permission'];
|
GithubComChaitinPandaWikiProApiTokenV1CreateAPITokenReq['permission'];
|
||||||
|
|
@ -69,8 +73,8 @@ const ApiToken = () => {
|
||||||
perm: ConstsUserKBPermission.UserKBPermissionFullControl,
|
perm: ConstsUserKBPermission.UserKBPermissionFullControl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const isEnterprise = useMemo(() => {
|
const isBusiness = useMemo(() => {
|
||||||
return license.edition === 2;
|
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||||
}, [license]);
|
}, [license]);
|
||||||
|
|
||||||
const onDeleteApiToken = (id: string, name: string) => {
|
const onDeleteApiToken = (id: string, name: string) => {
|
||||||
|
|
@ -131,9 +135,9 @@ const ApiToken = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!kb_id) return;
|
if (!kb_id || !isBusiness) return;
|
||||||
getApiTokenList();
|
getApiTokenList();
|
||||||
}, [kb_id]);
|
}, [kb_id, isBusiness]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!addOpen) reset();
|
if (!addOpen) reset();
|
||||||
|
|
@ -142,27 +146,17 @@ const ApiToken = () => {
|
||||||
return (
|
return (
|
||||||
<SettingCardItem
|
<SettingCardItem
|
||||||
title='API Token'
|
title='API Token'
|
||||||
|
permission={BUSINESS_VERSION_PERMISSION}
|
||||||
extra={
|
extra={
|
||||||
<Stack direction={'row'} alignItems={'center'}>
|
<Stack direction={'row'} alignItems={'center'}>
|
||||||
<Button
|
<Button
|
||||||
color='primary'
|
color='primary'
|
||||||
size='small'
|
size='small'
|
||||||
disabled={!isEnterprise}
|
|
||||||
onClick={() => setAddOpen(true)}
|
onClick={() => setAddOpen(true)}
|
||||||
sx={{ textTransform: 'none' }}
|
sx={{ textTransform: 'none' }}
|
||||||
>
|
>
|
||||||
创建 API Token
|
创建 API Token
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Tooltip title={'企业版可用'} placement='top' arrow>
|
|
||||||
<InfoIcon
|
|
||||||
sx={{
|
|
||||||
color: 'text.secondary',
|
|
||||||
fontSize: 14,
|
|
||||||
display: !isEnterprise ? 'block' : 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -232,7 +226,7 @@ const ApiToken = () => {
|
||||||
size='small'
|
size='small'
|
||||||
sx={{ width: 120 }}
|
sx={{ width: 120 }}
|
||||||
value={it.permission}
|
value={it.permission}
|
||||||
disabled={!isEnterprise || user.role !== 'admin'}
|
disabled={!isBusiness || user.role !== 'admin'}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
onUpdateApiToken(it.id!, e.target.value as ApiTokenPermission)
|
onUpdateApiToken(it.id!, e.target.value as ApiTokenPermission)
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +253,7 @@ const ApiToken = () => {
|
||||||
kbDetail?.perm !==
|
kbDetail?.perm !==
|
||||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||||
? '权限不足'
|
? '权限不足'
|
||||||
: '企业版可用'
|
: '商业版可用'
|
||||||
}
|
}
|
||||||
placement='top'
|
placement='top'
|
||||||
arrow
|
arrow
|
||||||
|
|
@ -270,7 +264,7 @@ const ApiToken = () => {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
ml: 1,
|
ml: 1,
|
||||||
visibility:
|
visibility:
|
||||||
!isEnterprise ||
|
!isBusiness ||
|
||||||
kbDetail?.perm !==
|
kbDetail?.perm !==
|
||||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||||
? 'visible'
|
? 'visible'
|
||||||
|
|
@ -285,13 +279,13 @@ const ApiToken = () => {
|
||||||
type='icon-icon_tool_close'
|
type='icon-icon_tool_close'
|
||||||
sx={{
|
sx={{
|
||||||
cursor:
|
cursor:
|
||||||
!isEnterprise ||
|
!isBusiness ||
|
||||||
kbDetail?.perm !==
|
kbDetail?.perm !==
|
||||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||||
? 'not-allowed'
|
? 'not-allowed'
|
||||||
: 'pointer',
|
: 'pointer',
|
||||||
color:
|
color:
|
||||||
!isEnterprise ||
|
!isBusiness ||
|
||||||
kbDetail?.perm !==
|
kbDetail?.perm !==
|
||||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||||
? 'text.disabled'
|
? 'text.disabled'
|
||||||
|
|
@ -299,7 +293,7 @@ const ApiToken = () => {
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (
|
if (
|
||||||
!isEnterprise ||
|
!isBusiness ||
|
||||||
kbDetail?.perm !==
|
kbDetail?.perm !==
|
||||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||||
)
|
)
|
||||||
|
|
@ -367,17 +361,16 @@ const ApiToken = () => {
|
||||||
>
|
>
|
||||||
完全控制
|
完全控制
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!isEnterprise}
|
|
||||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||||
>
|
>
|
||||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
文档管理
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={!isEnterprise}
|
|
||||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||||
>
|
>
|
||||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
数据运营
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
|
@ -405,9 +398,9 @@ const CardKB = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEnterprise = useMemo(() => {
|
const isPro = useMemo(() => {
|
||||||
return license.edition === 2;
|
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||||
}, [license]);
|
}, [license.edition]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!kb_id) return;
|
if (!kb_id) return;
|
||||||
|
|
@ -513,7 +506,7 @@ const CardKB = () => {
|
||||||
size='small'
|
size='small'
|
||||||
sx={{ width: 180 }}
|
sx={{ width: 180 }}
|
||||||
value={it.perms}
|
value={it.perms}
|
||||||
disabled={!isEnterprise || it.role === 'admin'}
|
disabled={!isPro || it.role === 'admin'}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
onUpdateUserPermission(
|
onUpdateUserPermission(
|
||||||
it.id!,
|
it.id!,
|
||||||
|
|
@ -542,7 +535,7 @@ const CardKB = () => {
|
||||||
title={
|
title={
|
||||||
it.role === 'admin'
|
it.role === 'admin'
|
||||||
? '超级管理员不可被修改权限'
|
? '超级管理员不可被修改权限'
|
||||||
: '企业版可用'
|
: '专业版可用'
|
||||||
}
|
}
|
||||||
placement='top'
|
placement='top'
|
||||||
arrow
|
arrow
|
||||||
|
|
@ -553,9 +546,7 @@ const CardKB = () => {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
ml: 1,
|
ml: 1,
|
||||||
visibility:
|
visibility:
|
||||||
!isEnterprise || it.role === 'admin'
|
!isPro || it.role === 'admin' ? 'visible' : 'hidden',
|
||||||
? 'visible'
|
|
||||||
: 'hidden',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,11 @@ import {
|
||||||
} from '@/request/types';
|
} from '@/request/types';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import { Icon, message } from '@ctzhian/ui';
|
import { Icon, message } from '@ctzhian/ui';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
|
Collapse,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Link,
|
Link,
|
||||||
Radio,
|
Radio,
|
||||||
|
|
@ -31,6 +34,8 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
const [isEnabled, setIsEnabled] = useState(false);
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
|
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 { kb_id } = useAppSelector(state => state.config);
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
|
|
@ -43,8 +48,15 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
is_open: 0,
|
is_open: 0,
|
||||||
theme_mode: 'light',
|
theme_mode: 'light',
|
||||||
|
btn_style: 'hover_ball',
|
||||||
|
btn_id: '',
|
||||||
|
btn_position: 'bottom_right',
|
||||||
|
disclaimer: '',
|
||||||
btn_text: '',
|
btn_text: '',
|
||||||
btn_logo: '',
|
btn_logo: '',
|
||||||
|
modal_position: 'follow',
|
||||||
|
search_mode: 'all',
|
||||||
|
placeholder: '',
|
||||||
recommend_questions: [] as string[],
|
recommend_questions: [] as string[],
|
||||||
recommend_node_ids: [] as string[],
|
recommend_node_ids: [] as string[],
|
||||||
},
|
},
|
||||||
|
|
@ -54,6 +66,8 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
|
|
||||||
const recommend_questions = watch('recommend_questions') || [];
|
const recommend_questions = watch('recommend_questions') || [];
|
||||||
const recommend_node_ids = watch('recommend_node_ids') || [];
|
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>({
|
const recommendQuestionsField = useCommitPendingInput<string>({
|
||||||
value: recommend_questions,
|
value: recommend_questions,
|
||||||
|
|
@ -87,8 +101,17 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
reset({
|
reset({
|
||||||
is_open: res.settings?.widget_bot_settings?.is_open ? 1 : 0,
|
is_open: res.settings?.widget_bot_settings?.is_open ? 1 : 0,
|
||||||
theme_mode: res.settings?.widget_bot_settings?.theme_mode || 'light',
|
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_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:
|
recommend_questions:
|
||||||
res.settings?.widget_bot_settings?.recommend_questions || [],
|
res.settings?.widget_bot_settings?.recommend_questions || [],
|
||||||
recommend_node_ids:
|
recommend_node_ids:
|
||||||
|
|
@ -108,8 +131,15 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
widget_bot_settings: {
|
widget_bot_settings: {
|
||||||
is_open: data.is_open === 1 ? true : false,
|
is_open: data.is_open === 1 ? true : false,
|
||||||
theme_mode: data.theme_mode as 'light' | 'dark',
|
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_text: data.btn_text,
|
||||||
btn_logo: data.btn_logo,
|
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_questions: data.recommend_questions || [],
|
||||||
recommend_node_ids: data.recommend_node_ids || [],
|
recommend_node_ids: data.recommend_node_ids || [],
|
||||||
},
|
},
|
||||||
|
|
@ -151,146 +181,469 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FormItem label='网页挂件机器人'>
|
<Stack spacing={3}>
|
||||||
<Controller
|
<FormItem label='网页挂件机器人'>
|
||||||
control={control}
|
<Controller
|
||||||
name='is_open'
|
control={control}
|
||||||
render={({ field }) => (
|
name='is_open'
|
||||||
<RadioGroup
|
render={({ field }) => (
|
||||||
row
|
<RadioGroup
|
||||||
{...field}
|
row
|
||||||
onChange={e => {
|
{...field}
|
||||||
field.onChange(+e.target.value as 1 | 0);
|
onChange={e => {
|
||||||
setIsEnabled((+e.target.value as 1 | 0) === 1);
|
field.onChange(+e.target.value as 1 | 0);
|
||||||
setIsEdit(true);
|
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
|
{url ? (
|
||||||
value={1}
|
<ShowText
|
||||||
control={<Radio size='small' />}
|
noEllipsis
|
||||||
label={<Box sx={{ width: 100 }}>启用</Box>}
|
text={[
|
||||||
/>
|
`<!--// Head 标签引入样式 -->`,
|
||||||
<FormControlLabel
|
`<link rel="stylesheet" href="${url}/widget-bot.css">`,
|
||||||
value={0}
|
`<!--// Body 标签引入挂件 -->`,
|
||||||
control={<Radio size='small' />}
|
`<script src="${url}/widget-bot.js"></script>`,
|
||||||
label={<Box sx={{ width: 100 }}>禁用</Box>}
|
]}
|
||||||
/>
|
/>
|
||||||
</RadioGroup>
|
) : (
|
||||||
)}
|
<Stack
|
||||||
/>
|
direction='row'
|
||||||
</FormItem>
|
alignItems={'center'}
|
||||||
{isEnabled && (
|
gap={0.5}
|
||||||
<>
|
sx={{
|
||||||
<FormItem label='配色方案'>
|
color: 'warning.main',
|
||||||
<Controller
|
fontSize: 14,
|
||||||
control={control}
|
p: 1.5,
|
||||||
name='theme_mode'
|
borderRadius: 1,
|
||||||
render={({ field }) => (
|
bgcolor: 'warning.light',
|
||||||
<RadioGroup
|
|
||||||
row
|
|
||||||
{...field}
|
|
||||||
onChange={e => {
|
|
||||||
field.onChange(e.target.value);
|
|
||||||
setIsEdit(true);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FormControlLabel
|
<Icon type='icon-jinggao' />
|
||||||
value={'light'}
|
未配置域名,可在右侧
|
||||||
control={<Radio size='small' />}
|
<Box component={'span'} sx={{ fontWeight: 500 }}>
|
||||||
label={<Box sx={{ width: 100 }}>浅色模式</Box>}
|
服务监听方式
|
||||||
/>
|
</Box>{' '}
|
||||||
<FormControlLabel
|
中配置
|
||||||
value={'dark'}
|
</Stack>
|
||||||
control={<Radio size='small' />}
|
|
||||||
label={<Box sx={{ width: 100 }}>深色模式</Box>}
|
|
||||||
/>
|
|
||||||
</RadioGroup>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</FormItem>
|
||||||
</FormItem>
|
<FormItem
|
||||||
<FormItem label='侧边按钮文字'>
|
label='挂件配置'
|
||||||
<Controller
|
sx={{ alignItems: 'flex-start' }}
|
||||||
control={control}
|
labelSx={{ mt: 1 }}
|
||||||
name='btn_text'
|
>
|
||||||
render={({ field }) => (
|
<Box>
|
||||||
<TextField
|
{!widgetConfigOpen && (
|
||||||
fullWidth
|
<Button
|
||||||
{...field}
|
size='small'
|
||||||
placeholder='输入侧边按钮文字'
|
variant='outlined'
|
||||||
error={!!errors.btn_text}
|
onClick={() => setWidgetConfigOpen(true)}
|
||||||
helperText={errors.btn_text?.message}
|
endIcon={<ExpandMoreIcon />}
|
||||||
onChange={event => {
|
>
|
||||||
setIsEdit(true);
|
展开
|
||||||
field.onChange(event);
|
</Button>
|
||||||
}}
|
)}
|
||||||
/>
|
<Collapse in={widgetConfigOpen}>
|
||||||
)}
|
<Stack spacing={2.5}>
|
||||||
/>
|
<FormItem
|
||||||
</FormItem>
|
label='按钮样式'
|
||||||
<FormItem label='侧边按钮 Logo'>
|
sx={{ alignItems: 'flex-start' }}
|
||||||
<Controller
|
labelSx={{ mt: 1 }}
|
||||||
control={control}
|
>
|
||||||
name='btn_logo'
|
<Controller
|
||||||
render={({ field }) => (
|
control={control}
|
||||||
<UploadFile
|
name='btn_style'
|
||||||
{...field}
|
render={({ field }) => (
|
||||||
id='btn_logo'
|
<RadioGroup
|
||||||
type='url'
|
row
|
||||||
accept='image/*'
|
{...field}
|
||||||
width={80}
|
onChange={e => {
|
||||||
onChange={url => {
|
const value = e.target.value;
|
||||||
field.onChange(url);
|
field.onChange(value);
|
||||||
setIsEdit(true);
|
if (value === 'btn_trigger') {
|
||||||
}}
|
setValue('modal_position', 'fixed');
|
||||||
/>
|
}
|
||||||
)}
|
setIsEdit(true);
|
||||||
/>
|
}}
|
||||||
</FormItem>
|
>
|
||||||
<FormItem label='推荐问题'>
|
<FormControlLabel
|
||||||
<FreeSoloAutocomplete
|
value='hover_ball'
|
||||||
{...recommendQuestionsField}
|
control={<Radio size='small' />}
|
||||||
placeholder='回车确认,填写下一个推荐问题'
|
label={<Box sx={{ width: 100 }}>悬浮球</Box>}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
<FormControlLabel
|
||||||
<FormItem label='推荐文档'>
|
value='side_sticky'
|
||||||
<RecommendDocDragList
|
control={<Radio size='small' />}
|
||||||
ids={recommend_node_ids}
|
label={<Box sx={{ width: 100 }}>侧边吸附</Box>}
|
||||||
onChange={(value: string[]) => {
|
/>
|
||||||
setIsEdit(true);
|
<FormControlLabel
|
||||||
setValue('recommend_node_ids', value);
|
value='btn_trigger'
|
||||||
}}
|
control={<Radio size='small' />}
|
||||||
/>
|
label={<Box sx={{ width: 100 }}>自定义按钮</Box>}
|
||||||
</FormItem>
|
/>
|
||||||
<FormItem label='嵌入代码'>
|
</RadioGroup>
|
||||||
{url ? (
|
)}
|
||||||
<ShowText
|
/>
|
||||||
noEllipsis
|
</FormItem>
|
||||||
text={[
|
{isCustomButton ? (
|
||||||
`<!--// Head 标签引入样式 -->`,
|
<FormItem
|
||||||
`<link rel="stylesheet" href="${url}/widget-bot.css">`,
|
label='自定义按钮 ID'
|
||||||
'',
|
required
|
||||||
`<!--// Body 标签引入挂件 -->`,
|
sx={{ alignItems: 'flex-start' }}
|
||||||
`<script src="${url}/widget-bot.js"></script>`,
|
labelSx={{ mt: 1 }}
|
||||||
]}
|
>
|
||||||
/>
|
<Controller
|
||||||
) : (
|
control={control}
|
||||||
<Stack
|
name='btn_id'
|
||||||
direction='row'
|
rules={{
|
||||||
alignItems={'center'}
|
required: '自定义按钮 ID 不能为空',
|
||||||
gap={0.5}
|
}}
|
||||||
sx={{ color: 'warning.main', fontSize: 14 }}
|
render={({ field }) => (
|
||||||
>
|
<TextField
|
||||||
<Icon type='icon-jinggao' />
|
{...field}
|
||||||
未配置域名,可在右侧
|
fullWidth
|
||||||
<Box component={'span'} sx={{ fontWeight: 500 }}>
|
placeholder='嵌入网站中自定义按钮的 #id 点击触发,如: pandawiki-widget-bot-btn'
|
||||||
服务监听方式
|
error={!!errors.btn_id}
|
||||||
</Box>{' '}
|
helperText={errors.btn_id?.message}
|
||||||
中配置
|
onChange={event => {
|
||||||
</Stack>
|
setIsEdit(true);
|
||||||
)}
|
field.onChange(event);
|
||||||
</FormItem>
|
}}
|
||||||
</>
|
/>
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
|
</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>
|
</SettingCardItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { DomainKnowledgeBaseDetail } from '@/request/types';
|
import { DomainKnowledgeBaseDetail } from '@/request/types';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Link,
|
Link,
|
||||||
|
|
@ -13,10 +12,11 @@ import {
|
||||||
import ShowText from '@/components/ShowText';
|
import ShowText from '@/components/ShowText';
|
||||||
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FormItem, SettingCardItem } from './Common';
|
import { FormItem, SettingCardItem } from './Common';
|
||||||
import { DomainAppDetailResp } from '@/request/types';
|
import { DomainAppDetailResp } from '@/request/types';
|
||||||
import { message } from '@ctzhian/ui';
|
import { message } from '@ctzhian/ui';
|
||||||
|
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
|
||||||
const CardRobotApi = ({
|
const CardRobotApi = ({
|
||||||
|
|
@ -29,11 +29,6 @@ const CardRobotApi = ({
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
|
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
|
||||||
const { license } = useAppSelector(state => state.config);
|
const { license } = useAppSelector(state => state.config);
|
||||||
|
|
||||||
const isEnterprise = useMemo(() => {
|
|
||||||
return license.edition === 2;
|
|
||||||
}, [license]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|
@ -114,10 +109,7 @@ const CardRobotApi = ({
|
||||||
}
|
}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
<FormItem
|
<FormItem label='问答机器人 API' permission={BUSINESS_VERSION_PERMISSION}>
|
||||||
label='问答机器人 API'
|
|
||||||
tooltip={!isEnterprise ? '企业版可用' : undefined}
|
|
||||||
>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|
@ -133,13 +125,11 @@ const CardRobotApi = ({
|
||||||
<Stack direction={'row'}>
|
<Stack direction={'row'}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={true}
|
value={true}
|
||||||
disabled={!isEnterprise}
|
|
||||||
control={<Radio size='small' />}
|
control={<Radio size='small' />}
|
||||||
label={<Box sx={{ width: 100 }}>启用</Box>}
|
label={<Box sx={{ width: 100 }}>启用</Box>}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={false}
|
value={false}
|
||||||
disabled={!isEnterprise}
|
|
||||||
control={<Radio size='small' />}
|
control={<Radio size='small' />}
|
||||||
label={<Box sx={{ width: 100 }}>禁用</Box>}
|
label={<Box sx={{ width: 100 }}>禁用</Box>}
|
||||||
/>
|
/>
|
||||||
|
|
@ -150,7 +140,7 @@ const CardRobotApi = ({
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
{isEnabled && (
|
{isEnabled && BUSINESS_VERSION_PERMISSION.includes(license.edition!) && (
|
||||||
<>
|
<>
|
||||||
<FormItem label='API Token' required>
|
<FormItem label='API Token' required>
|
||||||
<Controller
|
<Controller
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import {
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { FormItem, SettingCardItem } from './Common';
|
import { FormItem, SettingCardItem } from './Common';
|
||||||
|
import VersionMask from '@/components/VersionMask';
|
||||||
|
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||||
|
|
||||||
const CardRobotWecomService = ({
|
const CardRobotWecomService = ({
|
||||||
kb,
|
kb,
|
||||||
|
|
@ -262,38 +264,40 @@ const CardRobotWecomService = ({
|
||||||
<Icon type='icon-jinggao' sx={{ fontSize: 18 }} />
|
<Icon type='icon-jinggao' sx={{ fontSize: 18 }} />
|
||||||
人工客服转接配置:当用户触发以下场景时,会自动转接人工客服
|
人工客服转接配置:当用户触发以下场景时,会自动转接人工客服
|
||||||
</Stack>
|
</Stack>
|
||||||
<FormItem
|
<VersionMask permission={PROFESSION_VERSION_PERMISSION}>
|
||||||
label={
|
<FormItem
|
||||||
<Box>
|
label={
|
||||||
提问
|
<Box>
|
||||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
提问
|
||||||
包含特定
|
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||||
|
包含特定
|
||||||
|
</Box>
|
||||||
|
关键词
|
||||||
</Box>
|
</Box>
|
||||||
关键词
|
}
|
||||||
</Box>
|
>
|
||||||
}
|
<FreeSoloAutocomplete
|
||||||
>
|
placeholder='回车确认,填写下一个'
|
||||||
<FreeSoloAutocomplete
|
{...containKeywordsField}
|
||||||
placeholder='回车确认,填写下一个'
|
/>
|
||||||
{...containKeywordsField}
|
</FormItem>
|
||||||
/>
|
<FormItem
|
||||||
</FormItem>
|
label={
|
||||||
<FormItem
|
<Box>
|
||||||
label={
|
提问
|
||||||
<Box>
|
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||||
提问
|
完全匹配
|
||||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
</Box>
|
||||||
完全匹配
|
关键词
|
||||||
</Box>
|
</Box>
|
||||||
关键词
|
}
|
||||||
</Box>
|
>
|
||||||
}
|
<FreeSoloAutocomplete
|
||||||
>
|
placeholder='回车确认,填写下一个'
|
||||||
<FreeSoloAutocomplete
|
{...equalKeywordsField}
|
||||||
placeholder='回车确认,填写下一个'
|
/>
|
||||||
{...equalKeywordsField}
|
</FormItem>
|
||||||
/>
|
</VersionMask>
|
||||||
</FormItem>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SettingCardItem>
|
</SettingCardItem>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
} from '@/request/types';
|
} from '@/request/types';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
import { message } from '@ctzhian/ui';
|
import { message } from '@ctzhian/ui';
|
||||||
|
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||||
import {
|
import {
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
Box,
|
Box,
|
||||||
|
|
@ -17,7 +18,7 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
styled,
|
styled,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { FormItem, SettingCardItem } from './Common';
|
import { FormItem, SettingCardItem } from './Common';
|
||||||
|
|
||||||
|
|
@ -32,15 +33,9 @@ const WatermarkForm = ({
|
||||||
data?: DomainAppDetailResp;
|
data?: DomainAppDetailResp;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { license, kb_id } = useAppSelector(state => state.config);
|
const { kb_id } = useAppSelector(state => state.config);
|
||||||
const [watermarkIsEdit, setWatermarkIsEdit] = useState(false);
|
const [watermarkIsEdit, setWatermarkIsEdit] = useState(false);
|
||||||
const {
|
const { control, handleSubmit, setValue, watch } = useForm({
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
setValue,
|
|
||||||
watch,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
watermark_setting: data?.settings?.watermark_setting ?? null,
|
watermark_setting: data?.settings?.watermark_setting ?? null,
|
||||||
watermark_content: data?.settings?.watermark_content ?? '',
|
watermark_content: data?.settings?.watermark_content ?? '',
|
||||||
|
|
@ -48,9 +43,6 @@ const WatermarkForm = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const watermarkSetting = watch('watermark_setting');
|
const watermarkSetting = watch('watermark_setting');
|
||||||
const isEnterprise = useMemo(() => {
|
|
||||||
return license.edition === 2;
|
|
||||||
}, [license]);
|
|
||||||
|
|
||||||
const handleSaveWatermark = handleSubmit(values => {
|
const handleSaveWatermark = handleSubmit(values => {
|
||||||
if (!data?.id || values.watermark_setting === null) return;
|
if (!data?.id || values.watermark_setting === null) return;
|
||||||
|
|
@ -82,8 +74,9 @@ const WatermarkForm = ({
|
||||||
title='水印'
|
title='水印'
|
||||||
isEdit={watermarkIsEdit}
|
isEdit={watermarkIsEdit}
|
||||||
onSubmit={handleSaveWatermark}
|
onSubmit={handleSaveWatermark}
|
||||||
|
permission={BUSINESS_VERSION_PERMISSION}
|
||||||
>
|
>
|
||||||
<FormItem label='水印开关' tooltip={!isEnterprise && '企业版可用'}>
|
<FormItem label='水印开关'>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='watermark_setting'
|
name='watermark_setting'
|
||||||
|
|
@ -98,18 +91,18 @@ const WatermarkForm = ({
|
||||||
>
|
>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={ConstsWatermarkSetting.WatermarkVisible}
|
value={ConstsWatermarkSetting.WatermarkVisible}
|
||||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
control={<Radio size='small' />}
|
||||||
label={<StyledRadioLabel>显性水印</StyledRadioLabel>}
|
label={<StyledRadioLabel>显性水印</StyledRadioLabel>}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={ConstsWatermarkSetting.WatermarkHidden}
|
value={ConstsWatermarkSetting.WatermarkHidden}
|
||||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
control={<Radio size='small' />}
|
||||||
label={<StyledRadioLabel>隐形水印</StyledRadioLabel>}
|
label={<StyledRadioLabel>隐形水印</StyledRadioLabel>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={ConstsWatermarkSetting.WatermarkDisabled}
|
value={ConstsWatermarkSetting.WatermarkDisabled}
|
||||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
control={<Radio size='small' />}
|
||||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
@ -128,7 +121,6 @@ const WatermarkForm = ({
|
||||||
placeholder='请输入水印内容, 支持多行输入'
|
placeholder='请输入水印内容, 支持多行输入'
|
||||||
multiline
|
multiline
|
||||||
minRows={2}
|
minRows={2}
|
||||||
disabled={!isEnterprise}
|
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setWatermarkIsEdit(true);
|
setWatermarkIsEdit(true);
|
||||||
field.onChange(e.target.value);
|
field.onChange(e.target.value);
|
||||||
|
|
@ -146,9 +138,6 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
||||||
const { license } = useAppSelector(state => state.config);
|
const { license } = useAppSelector(state => state.config);
|
||||||
const [questionInputValue, setQuestionInputValue] = useState('');
|
const [questionInputValue, setQuestionInputValue] = useState('');
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
const isEnterprise = useMemo(() => {
|
|
||||||
return license.edition === 2;
|
|
||||||
}, [license]);
|
|
||||||
|
|
||||||
const { control, handleSubmit, setValue } = useForm({
|
const { control, handleSubmit, setValue } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|
@ -169,17 +158,18 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!kb.id || !isEnterprise) return;
|
if (!kb.id || !BUSINESS_VERSION_PERMISSION.includes(license.edition!))
|
||||||
|
return;
|
||||||
getApiProV1Block({ kb_id: kb.id! }).then(res => {
|
getApiProV1Block({ kb_id: kb.id! }).then(res => {
|
||||||
setValue('block_words', res.words || []);
|
setValue('block_words', res.words || []);
|
||||||
});
|
});
|
||||||
}, [kb, isEnterprise]);
|
}, [kb, license.edition]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingCardItem title='内容合规' isEdit={isEdit} onSubmit={onSubmit}>
|
<SettingCardItem title='内容合规' isEdit={isEdit} onSubmit={onSubmit}>
|
||||||
<FormItem
|
<FormItem
|
||||||
vertical
|
vertical
|
||||||
tooltip={!isEnterprise && '企业版可用'}
|
permission={BUSINESS_VERSION_PERMISSION}
|
||||||
label='屏蔽 AI 问答中的关键字'
|
label='屏蔽 AI 问答中的关键字'
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
|
|
@ -193,7 +183,6 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
||||||
inputValue={questionInputValue}
|
inputValue={questionInputValue}
|
||||||
options={[]}
|
options={[]}
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={!isEnterprise}
|
|
||||||
onInputChange={(_, value) => {
|
onInputChange={(_, value) => {
|
||||||
setQuestionInputValue(value);
|
setQuestionInputValue(value);
|
||||||
}}
|
}}
|
||||||
|
|
@ -234,23 +223,14 @@ const CopyForm = ({
|
||||||
data?: DomainAppDetailResp;
|
data?: DomainAppDetailResp;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { license, kb_id } = useAppSelector(state => state.config);
|
const { kb_id } = useAppSelector(state => state.config);
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
const {
|
const { control, handleSubmit, setValue } = useForm({
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
setValue,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
copy_setting: data?.settings?.copy_setting ?? null,
|
copy_setting: data?.settings?.copy_setting ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const isEnterprise = useMemo(() => {
|
|
||||||
return license.edition === 2;
|
|
||||||
}, [license]);
|
|
||||||
|
|
||||||
const handleSaveWatermark = handleSubmit(values => {
|
const handleSaveWatermark = handleSubmit(values => {
|
||||||
if (!data?.id || values.copy_setting === null) return;
|
if (!data?.id || values.copy_setting === null) return;
|
||||||
putApiV1App(
|
putApiV1App(
|
||||||
|
|
@ -280,7 +260,7 @@ const CopyForm = ({
|
||||||
isEdit={isEdit}
|
isEdit={isEdit}
|
||||||
onSubmit={handleSaveWatermark}
|
onSubmit={handleSaveWatermark}
|
||||||
>
|
>
|
||||||
<FormItem label='限制复制' tooltip={!isEnterprise && '企业版可用'}>
|
<FormItem label='限制复制' permission={BUSINESS_VERSION_PERMISSION}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='copy_setting'
|
name='copy_setting'
|
||||||
|
|
@ -295,18 +275,18 @@ const CopyForm = ({
|
||||||
>
|
>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={ConstsCopySetting.CopySettingNone}
|
value={ConstsCopySetting.CopySettingNone}
|
||||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
control={<Radio size='small' />}
|
||||||
label={<StyledRadioLabel>不做限制</StyledRadioLabel>}
|
label={<StyledRadioLabel>不做限制</StyledRadioLabel>}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={ConstsCopySetting.CopySettingAppend}
|
value={ConstsCopySetting.CopySettingAppend}
|
||||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
control={<Radio size='small' />}
|
||||||
label={<StyledRadioLabel>增加内容尾巴</StyledRadioLabel>}
|
label={<StyledRadioLabel>增加内容尾巴</StyledRadioLabel>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
value={ConstsCopySetting.CopySettingDisabled}
|
value={ConstsCopySetting.CopySettingDisabled}
|
||||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
control={<Radio size='small' />}
|
||||||
label={<StyledRadioLabel>禁止复制内容</StyledRadioLabel>}
|
label={<StyledRadioLabel>禁止复制内容</StyledRadioLabel>}
|
||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
desc: '',
|
desc: '',
|
||||||
keyword: '',
|
keyword: '',
|
||||||
auto_sitemap: false,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -44,7 +43,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue('desc', data.settings?.desc || '');
|
setValue('desc', data.settings?.desc || '');
|
||||||
setValue('keyword', data.settings?.keyword || '');
|
setValue('keyword', data.settings?.keyword || '');
|
||||||
setValue('auto_sitemap', data.settings?.auto_sitemap ?? false);
|
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -88,25 +86,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem label='自动生成 Sitemap'>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name='auto_sitemap'
|
|
||||||
render={({ field }) => (
|
|
||||||
<Checkbox
|
|
||||||
{...field}
|
|
||||||
checked={field.value}
|
|
||||||
size='small'
|
|
||||||
sx={{ p: 0, m: 0 }}
|
|
||||||
onChange={event => {
|
|
||||||
setIsEdit(true);
|
|
||||||
field.onChange(event);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</SettingCardItem>
|
</SettingCardItem>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
import { ConstsLicenseEdition } from '@/request/types';
|
||||||
import InfoIcon from '@mui/icons-material/Info';
|
import InfoIcon from '@mui/icons-material/Info';
|
||||||
import { Button, Stack, styled, SxProps, Tooltip } from '@mui/material';
|
import { Button, Stack, styled, SxProps, Tooltip } from '@mui/material';
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
import VersionMask from '@/components/VersionMask';
|
||||||
|
|
||||||
const StyledForm = styled('form')<{ gap?: number | string }>(
|
const StyledForm = styled('form')<{ gap?: number | string }>(
|
||||||
({ theme, gap = 2 }) => ({
|
({ theme, gap = 2 }) => ({
|
||||||
|
|
@ -40,6 +42,7 @@ const StyledFormLabel = styled('span')<{ required?: boolean }>(
|
||||||
|
|
||||||
export const StyledFormItem = styled('div')<{ vertical?: boolean }>(
|
export const StyledFormItem = styled('div')<{ vertical?: boolean }>(
|
||||||
({ theme, vertical }) => ({
|
({ theme, vertical }) => ({
|
||||||
|
position: 'relative',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: vertical ? 'flex-start' : 'center',
|
alignItems: vertical ? 'flex-start' : 'center',
|
||||||
flexDirection: vertical ? 'column' : 'row',
|
flexDirection: vertical ? 'column' : 'row',
|
||||||
|
|
@ -82,6 +85,7 @@ export const FormItem = ({
|
||||||
extra,
|
extra,
|
||||||
sx,
|
sx,
|
||||||
labelSx,
|
labelSx,
|
||||||
|
permission,
|
||||||
}: {
|
}: {
|
||||||
label?: string | React.ReactNode;
|
label?: string | React.ReactNode;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
@ -92,31 +96,37 @@ export const FormItem = ({
|
||||||
extra?: React.ReactNode;
|
extra?: React.ReactNode;
|
||||||
sx?: SxProps;
|
sx?: SxProps;
|
||||||
labelSx?: SxProps;
|
labelSx?: SxProps;
|
||||||
|
permission?: number[];
|
||||||
}) => {
|
}) => {
|
||||||
const { vertical: verticalContext, labelWidth: labelWidthContext } =
|
const { vertical: verticalContext, labelWidth: labelWidthContext } =
|
||||||
useContext(FormContext);
|
useContext(FormContext);
|
||||||
return (
|
|
||||||
<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}
|
return (
|
||||||
</StyledFormLabelWrapper>
|
<VersionMask permission={permission}>
|
||||||
{children}
|
<StyledFormItem vertical={vertical || verticalContext} sx={sx}>
|
||||||
</StyledFormItem>
|
<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 }) => ({
|
const StyledSettingCardItem = styled('div')(({ theme }) => ({
|
||||||
|
position: 'relative',
|
||||||
'&:not(:last-child)': {
|
'&:not(:last-child)': {
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
paddingBottom: theme.spacing(4),
|
paddingBottom: theme.spacing(4),
|
||||||
|
|
@ -204,6 +215,12 @@ export const SettingCardItem = ({
|
||||||
extra,
|
extra,
|
||||||
more,
|
more,
|
||||||
sx,
|
sx,
|
||||||
|
permission = [
|
||||||
|
ConstsLicenseEdition.LicenseEditionFree,
|
||||||
|
ConstsLicenseEdition.LicenseEditionProfession,
|
||||||
|
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||||
|
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||||
|
],
|
||||||
}: {
|
}: {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
|
|
@ -212,6 +229,7 @@ export const SettingCardItem = ({
|
||||||
extra?: React.ReactNode;
|
extra?: React.ReactNode;
|
||||||
more?: SettingCardItemMore;
|
more?: SettingCardItemMore;
|
||||||
sx?: SxProps;
|
sx?: SxProps;
|
||||||
|
permission?: number[];
|
||||||
}) => {
|
}) => {
|
||||||
const renderMore = (more: SettingCardItemMore) => {
|
const renderMore = (more: SettingCardItemMore) => {
|
||||||
if (more && typeof more === 'object' && 'type' in more) {
|
if (more && typeof more === 'object' && 'type' in more) {
|
||||||
|
|
@ -237,20 +255,23 @@ export const SettingCardItem = ({
|
||||||
return more;
|
return more;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSettingCardItem sx={sx}>
|
<VersionMask permission={permission}>
|
||||||
<StyledSettingCardItemTitleWrapper>
|
<StyledSettingCardItem sx={sx}>
|
||||||
<StyledSettingCardItemTitle>
|
<StyledSettingCardItemTitleWrapper>
|
||||||
{title} {renderMore(more)}
|
<StyledSettingCardItemTitle>
|
||||||
</StyledSettingCardItemTitle>
|
{title} {renderMore(more)}
|
||||||
{isEdit && (
|
</StyledSettingCardItemTitle>
|
||||||
<Button variant='contained' size='small' onClick={onSubmit}>
|
{isEdit && (
|
||||||
保存
|
<Button variant='contained' size='small' onClick={onSubmit}>
|
||||||
</Button>
|
保存
|
||||||
)}
|
</Button>
|
||||||
{extra}
|
)}
|
||||||
</StyledSettingCardItemTitleWrapper>
|
{extra}
|
||||||
<StyledSettingCardItemContent>{children}</StyledSettingCardItemContent>
|
</StyledSettingCardItemTitleWrapper>
|
||||||
</StyledSettingCardItem>
|
<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 { SettingCardItem } from '../Common';
|
||||||
import { Tooltip } from '@mui/material';
|
|
||||||
import InfoIcon from '@mui/icons-material/Info';
|
|
||||||
import { Modal, message } from '@ctzhian/ui';
|
import { Modal, message } from '@ctzhian/ui';
|
||||||
import { Stack, Button } from '@mui/material';
|
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { postApiProV1AuthGroupSync } from '@/request/pro/AuthOrg';
|
import { postApiProV1AuthGroupSync } from '@/request/pro/AuthOrg';
|
||||||
import {
|
import {
|
||||||
|
|
@ -19,6 +16,7 @@ import {
|
||||||
deleteApiProV1AuthGroupDelete,
|
deleteApiProV1AuthGroupDelete,
|
||||||
} from '@/request/pro/AuthGroup';
|
} from '@/request/pro/AuthGroup';
|
||||||
import GroupTree from './GroupTree';
|
import GroupTree from './GroupTree';
|
||||||
|
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||||
|
|
||||||
interface UserGroupProps {
|
interface UserGroupProps {
|
||||||
enabled: string;
|
enabled: string;
|
||||||
|
|
@ -45,10 +43,6 @@ const UserGroup = ({
|
||||||
GithubComChaitinPandaWikiProApiAuthV1AuthGroupTreeItem[]
|
GithubComChaitinPandaWikiProApiAuthV1AuthGroupTreeItem[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const isEnterprise = useMemo(() => {
|
|
||||||
return license.edition === 2;
|
|
||||||
}, [license]);
|
|
||||||
|
|
||||||
const onDeleteUserGroup = (id: number) => {
|
const onDeleteUserGroup = (id: number) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '删除用户组',
|
title: '删除用户组',
|
||||||
|
|
@ -74,10 +68,15 @@ const UserGroup = ({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!kb_id || enabled !== '2' || !isEnterprise) return;
|
if (
|
||||||
|
!kb_id ||
|
||||||
|
enabled !== '2' ||
|
||||||
|
!BUSINESS_VERSION_PERMISSION.includes(license.edition!)
|
||||||
|
)
|
||||||
|
return;
|
||||||
getUserGroup();
|
getUserGroup();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [kb_id, enabled, isEnterprise]);
|
}, [kb_id, enabled, license.edition!]);
|
||||||
|
|
||||||
const handleMove = async ({
|
const handleMove = async ({
|
||||||
id,
|
id,
|
||||||
|
|
@ -123,32 +122,7 @@ const UserGroup = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingCardItem
|
<SettingCardItem title='用户组' permission={BUSINESS_VERSION_PERMISSION}>
|
||||||
title='用户组'
|
|
||||||
more={
|
|
||||||
!isEnterprise && (
|
|
||||||
<Tooltip title='企业版可用' placement='top' arrow>
|
|
||||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }} />
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// extra={
|
|
||||||
// isEnterprise &&
|
|
||||||
// [
|
|
||||||
// ConstsSourceType.SourceTypeWeCom,
|
|
||||||
// ConstsSourceType.SourceTypeDingTalk,
|
|
||||||
// ].includes(sourceType as ConstsSourceType) && (
|
|
||||||
// <Button
|
|
||||||
// color='primary'
|
|
||||||
// size='small'
|
|
||||||
// onClick={handleSync}
|
|
||||||
// loading={syncLoading}
|
|
||||||
// >
|
|
||||||
// 同步组织架构和成员
|
|
||||||
// </Button>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
border: '1px dashed',
|
border: '1px dashed',
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ import QAReferer from './QAReferer';
|
||||||
import RTVisitor from './RTVisitor';
|
import RTVisitor from './RTVisitor';
|
||||||
import TypeCount from './TypeCount';
|
import TypeCount from './TypeCount';
|
||||||
import { useAppSelector } from '@/store';
|
import { useAppSelector } from '@/store';
|
||||||
|
import { VersionCanUse } from '@/components/VersionMask';
|
||||||
|
import {
|
||||||
|
BUSINESS_VERSION_PERMISSION,
|
||||||
|
PROFESSION_VERSION_PERMISSION,
|
||||||
|
} from '@/constant/version';
|
||||||
|
|
||||||
export const TimeList = [
|
export const TimeList = [
|
||||||
{ label: '近 24 小时', value: 1 },
|
{ label: '近 24 小时', value: 1 },
|
||||||
|
|
@ -25,13 +30,40 @@ const Statistic = () => {
|
||||||
const isWideScreen = useMediaQuery('(min-width:1190px)');
|
const isWideScreen = useMediaQuery('(min-width:1190px)');
|
||||||
|
|
||||||
const timeList = useMemo(() => {
|
const timeList = useMemo(() => {
|
||||||
const isPro = license.edition === 1 || license.edition === 2;
|
const isPro = PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||||
const isEnterprise = license.edition === 2;
|
const isBusiness = BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||||
return [
|
return [
|
||||||
{ label: '近 24 小时', value: 1, disabled: false },
|
{ label: '近 24 小时', value: 1, disabled: false },
|
||||||
{ label: '近 7 天', value: 7, disabled: !isPro },
|
{
|
||||||
{ label: '近 30 天', value: 30, disabled: !isEnterprise },
|
label: (
|
||||||
{ label: '近 90 天', value: 90, disabled: !isEnterprise },
|
<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]);
|
}, [license]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ import {
|
||||||
GetApiV1NodeListParams,
|
GetApiV1NodeListParams,
|
||||||
GetApiV1NodeRecommendNodesParams,
|
GetApiV1NodeRecommendNodesParams,
|
||||||
V1NodeDetailResp,
|
V1NodeDetailResp,
|
||||||
|
V1NodeRestudyReq,
|
||||||
|
V1NodeRestudyResp,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -263,6 +265,38 @@ export const getApiV1NodeRecommendNodes = (
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 文档重新学习
|
||||||
|
*
|
||||||
|
* @tags Node
|
||||||
|
* @name PostApiV1NodeRestudy
|
||||||
|
* @summary 文档重新学习
|
||||||
|
* @request POST:/api/v1/node/restudy
|
||||||
|
* @secure
|
||||||
|
* @response `200` `(DomainResponse & {
|
||||||
|
data?: V1NodeRestudyResp,
|
||||||
|
|
||||||
|
})` OK
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const postApiV1NodeRestudy = (
|
||||||
|
param: V1NodeRestudyReq,
|
||||||
|
params: RequestParams = {},
|
||||||
|
) =>
|
||||||
|
httpRequest<
|
||||||
|
DomainResponse & {
|
||||||
|
data?: V1NodeRestudyResp;
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
path: `/api/v1/node/restudy`,
|
||||||
|
method: "POST",
|
||||||
|
body: param,
|
||||||
|
secure: true,
|
||||||
|
type: ContentType.Json,
|
||||||
|
format: "json",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Summary Node
|
* @description Summary Node
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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 './Model'
|
||||||
export * from './Node'
|
export * from './Node'
|
||||||
export * from './NodePermission'
|
export * from './NodePermission'
|
||||||
export * from './NodeRestudy'
|
|
||||||
export * from './Stat'
|
export * from './Stat'
|
||||||
export * from './User'
|
export * from './User'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
||||||
|
GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
||||||
|
|
@ -206,6 +207,32 @@ export const postShareProV1AuthLdap = (
|
||||||
...params,
|
...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登录
|
* @description OAuth登录
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,12 @@ export enum ConstsSourceType {
|
||||||
export enum ConstsLicenseEdition {
|
export enum ConstsLicenseEdition {
|
||||||
/** 开源版 */
|
/** 开源版 */
|
||||||
LicenseEditionFree = 0,
|
LicenseEditionFree = 0,
|
||||||
/** 联创版 */
|
/** 专业版 */
|
||||||
LicenseEditionContributor = 1,
|
LicenseEditionProfession = 1,
|
||||||
/** 企业版 */
|
/** 企业版 */
|
||||||
LicenseEditionEnterprise = 2,
|
LicenseEditionEnterprise = 2,
|
||||||
|
/** 商业版 */
|
||||||
|
LicenseEditionBusiness = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConstsContributeType {
|
export enum ConstsContributeType {
|
||||||
|
|
@ -455,6 +457,11 @@ export type GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp = Record<
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp = Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
|
||||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
||||||
kb_id?: string;
|
kb_id?: string;
|
||||||
redirect_url?: string;
|
redirect_url?: string;
|
||||||
|
|
@ -465,6 +472,7 @@ export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthWecomReq {
|
export interface GithubComChaitinPandaWikiProApiShareV1AuthWecomReq {
|
||||||
|
is_app?: boolean;
|
||||||
kb_id?: string;
|
kb_id?: string;
|
||||||
redirect_url?: string;
|
redirect_url?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -668,8 +676,6 @@ export interface GetApiProV1TokenListParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PostApiV1LicensePayload {
|
export interface PostApiV1LicensePayload {
|
||||||
/** license edition */
|
|
||||||
license_edition: "contributor" | "enterprise";
|
|
||||||
/** license type */
|
/** license type */
|
||||||
license_type: "file" | "code";
|
license_type: "file" | "code";
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -171,14 +171,21 @@ export enum ConstsNodeAccessPerm {
|
||||||
NodeAccessPermClosed = "closed",
|
NodeAccessPermClosed = "closed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ConstsModelSettingMode {
|
||||||
|
ModelSettingModeManual = "manual",
|
||||||
|
ModelSettingModeAuto = "auto",
|
||||||
|
}
|
||||||
|
|
||||||
/** @format int32 */
|
/** @format int32 */
|
||||||
export enum ConstsLicenseEdition {
|
export enum ConstsLicenseEdition {
|
||||||
/** 开源版 */
|
/** 开源版 */
|
||||||
LicenseEditionFree = 0,
|
LicenseEditionFree = 0,
|
||||||
/** 联创版 */
|
/** 专业版 */
|
||||||
LicenseEditionContributor = 1,
|
LicenseEditionProfession = 1,
|
||||||
/** 企业版 */
|
/** 企业版 */
|
||||||
LicenseEditionEnterprise = 2,
|
LicenseEditionEnterprise = 2,
|
||||||
|
/** 商业版 */
|
||||||
|
LicenseEditionBusiness = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConstsHomePageSetting {
|
export enum ConstsHomePageSetting {
|
||||||
|
|
@ -927,8 +934,10 @@ export interface DomainModelModeSetting {
|
||||||
auto_mode_api_key?: string;
|
auto_mode_api_key?: string;
|
||||||
/** 自定义对话模型名称 */
|
/** 自定义对话模型名称 */
|
||||||
chat_model?: string;
|
chat_model?: string;
|
||||||
|
/** 手动模式下嵌入模型是否更新 */
|
||||||
|
is_manual_embedding_updated?: boolean;
|
||||||
/** 模式: manual 或 auto */
|
/** 模式: manual 或 auto */
|
||||||
mode?: string;
|
mode?: ConstsModelSettingMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainMoveNodeReq {
|
export interface DomainMoveNodeReq {
|
||||||
|
|
@ -1180,6 +1189,17 @@ export interface DomainShareConversationMessage {
|
||||||
role?: SchemaRoleType;
|
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 {
|
export interface DomainSimpleAuth {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
|
@ -1357,11 +1377,18 @@ export interface DomainWecomAIBotSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainWidgetBotSettings {
|
export interface DomainWidgetBotSettings {
|
||||||
|
btn_id?: string;
|
||||||
btn_logo?: string;
|
btn_logo?: string;
|
||||||
|
btn_position?: string;
|
||||||
|
btn_style?: string;
|
||||||
btn_text?: string;
|
btn_text?: string;
|
||||||
|
disclaimer?: string;
|
||||||
is_open?: boolean;
|
is_open?: boolean;
|
||||||
|
modal_position?: string;
|
||||||
|
placeholder?: string;
|
||||||
recommend_node_ids?: string[];
|
recommend_node_ids?: string[];
|
||||||
recommend_questions?: string[];
|
recommend_questions?: string[];
|
||||||
|
search_mode?: string;
|
||||||
theme_mode?: string;
|
theme_mode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1645,8 +1672,9 @@ export interface V1NodePermissionResp {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface V1NodeRestudyReq {
|
export interface V1NodeRestudyReq {
|
||||||
kb_id?: string;
|
kb_id: string;
|
||||||
node_ids?: string[];
|
/** @minItems 1 */
|
||||||
|
node_ids: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type V1NodeRestudyResp = Record<string, any>;
|
export type V1NodeRestudyResp = Record<string, any>;
|
||||||
|
|
@ -1666,6 +1694,7 @@ export interface V1ShareNodeDetailResp {
|
||||||
editor_id?: string;
|
editor_id?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
kb_id?: string;
|
kb_id?: string;
|
||||||
|
list?: DomainShareNodeListItemResp[];
|
||||||
meta?: DomainNodeMeta;
|
meta?: DomainNodeMeta;
|
||||||
name?: string;
|
name?: string;
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:20-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
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.
|
// 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.
|
// 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.
|
// side errors will fail.
|
||||||
tunnelRoute: '/monitoring',
|
tunnelRoute: '/monitoring',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 3010",
|
"dev": "next dev -p 3010",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
"@emoji-mart/data": "^1.2.1",
|
"@emoji-mart/data": "^1.2.1",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@emotion/cache": "^11.14.0",
|
"@emotion/cache": "^11.14.0",
|
||||||
"@mui/material-nextjs": "^7.1.0",
|
"@mui/material-nextjs": "^7.3.5",
|
||||||
"@sentry/nextjs": "^10.8.0",
|
"@sentry/nextjs": "^10.8.0",
|
||||||
"@types/markdown-it": "13.0.1",
|
"@types/markdown-it": "13.0.1",
|
||||||
"@vscode/markdown-it-katex": "^1.1.2",
|
"@vscode/markdown-it-katex": "^1.1.2",
|
||||||
|
|
@ -25,12 +25,13 @@
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"html-react-parser": "^5.2.5",
|
"html-react-parser": "^5.2.5",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
|
"import-in-the-middle": "^1.4.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"katex": "^0.16.22",
|
"katex": "^0.16.22",
|
||||||
"markdown-it": "13.0.1",
|
"markdown-it": "13.0.1",
|
||||||
"markdown-it-highlightjs": "^4.2.0",
|
"markdown-it-highlightjs": "^4.2.0",
|
||||||
"mermaid": "^11.9.0",
|
"mermaid": "^11.9.0",
|
||||||
"next": "15.4.6",
|
"next": "^16.0.0",
|
||||||
"react-device-detect": "^2.2.3",
|
"react-device-detect": "^2.2.3",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-photo-view": "^1.2.7",
|
"react-photo-view": "^1.2.7",
|
||||||
|
|
@ -41,17 +42,23 @@
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
"require-in-the-middle": "^7.5.2",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ctzhian/cx-swagger-api": "^1.0.0",
|
"@ctzhian/cx-swagger-api": "^1.0.0",
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@next/eslint-plugin-next": "^15.4.5",
|
"@next/eslint-plugin-next": "^16.0.0",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/rangy": "^1.3.0",
|
"@types/rangy": "^1.3.0",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@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"
|
"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 {
|
.widget-bot-button {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
|
||||||
bottom: 190px;
|
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
border-radius: 18px 0 0 18px;
|
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
box-shadow: 0px 6px 10px 0px rgba(54, 59, 76, 0.17);
|
|
||||||
padding: 11px;
|
|
||||||
min-height: 120px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
|
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
|
||||||
border: none;
|
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);
|
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 {
|
.widget-bot-button.dragging {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
transform: rotate(2deg);
|
transition: none !important;
|
||||||
box-shadow: 0 6px 12px rgba(50, 72, 242, 0.25);
|
/* 拖拽时禁用过渡,提升性能 */
|
||||||
|
/* transform 由 JS 控制,包含 rotate 和 translate */
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-button-content {
|
.widget-bot-button-content {
|
||||||
|
|
@ -39,14 +43,13 @@
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-logo {
|
/* 图标样式 */
|
||||||
width: 20px;
|
.widget-bot-icon {
|
||||||
height: 20px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 文字样式 */
|
||||||
.widget-bot-text {
|
.widget-bot-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
@ -60,6 +63,47 @@
|
||||||
margin: 1px 0;
|
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主题 */
|
/* 模态框样式 - 基于MUI主题 */
|
||||||
.widget-bot-modal {
|
.widget-bot-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -75,6 +119,11 @@
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.widget-bot-modal-fixed {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.widget-bot-modal-content {
|
.widget-bot-modal-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 600px;
|
width: 600px;
|
||||||
|
|
@ -88,6 +137,14 @@
|
||||||
animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
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 {
|
@keyframes slideInUp {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -100,34 +157,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 关闭按钮样式 - 基于MUI IconButton */
|
/* 关闭按钮样式 - 透明框 */
|
||||||
.widget-bot-close-btn {
|
.widget-bot-close-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
top: 22.5px;
|
||||||
right: 12px;
|
right: 16px;
|
||||||
background: none;
|
background: transparent;
|
||||||
width: 36px;
|
width: 36.26px;
|
||||||
height: 36px;
|
height: 25px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
border-radius: 0;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 18px;
|
font-size: 0;
|
||||||
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 {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
z-index: 10001;
|
||||||
|
transition: none;
|
||||||
.widget-bot-close-btn:active {
|
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||||
transform: scale(0.95);
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
/* 允许鼠标穿透到下方 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iframe样式 */
|
/* iframe样式 */
|
||||||
|
|
@ -140,6 +193,11 @@
|
||||||
background: #F8F9FA;
|
background: #F8F9FA;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.widget-bot-modal-content-fixed .widget-bot-iframe {
|
||||||
|
min-height: 600px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/* 防止页面滚动 */
|
/* 防止页面滚动 */
|
||||||
body.widget-bot-modal-open {
|
body.widget-bot-modal-open {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -147,19 +205,34 @@ body.widget-bot-modal-open {
|
||||||
|
|
||||||
/* 暗色主题支持 - 基于data-theme属性 */
|
/* 暗色主题支持 - 基于data-theme属性 */
|
||||||
.widget-bot-button[data-theme="dark"] {
|
.widget-bot-button[data-theme="dark"] {
|
||||||
background: #6E73FE;
|
|
||||||
box-shadow: 0 2px 4px rgba(110, 115, 254, 0.15);
|
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;
|
background: #5d68fd;
|
||||||
box-shadow: 0 4px 8px rgba(110, 115, 254, 0.2);
|
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);
|
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"] {
|
.widget-bot-modal[data-theme="dark"] {
|
||||||
background: rgba(0, 0, 0, 0.7);
|
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);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端适配 */
|
/* 移动端适配 - 统一处理 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.widget-bot-button {
|
.widget-bot-side-sticky {
|
||||||
bottom: 16px;
|
width: 48px;
|
||||||
padding: 8px;
|
padding: 6px 6px 12px 6px;
|
||||||
border-radius: 10px 0 0 10px;
|
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 {
|
.widget-bot-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-logo {
|
.widget-bot-icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端弹框统一居中显示,宽度100%-32px,高度90vh */
|
||||||
.widget-bot-modal-content {
|
.widget-bot-modal-content {
|
||||||
width: calc(100% - 60.5px);
|
position: relative !important;
|
||||||
height: 90%;
|
width: calc(100% - 32px) !important;
|
||||||
max-width: none;
|
height: 90vh !important;
|
||||||
max-height: none;
|
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 {
|
.widget-bot-close-btn {
|
||||||
top: 8px;
|
top: 22.5px;
|
||||||
right: 8px;
|
right: 16px;
|
||||||
width: 32px;
|
width: 36.26px;
|
||||||
height: 32px;
|
height: 25px;
|
||||||
font-size: 16px;
|
font-size: 0;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 小屏幕适配 */
|
|
||||||
@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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,19 +349,32 @@ body.widget-bot-modal-open {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 浅色主题样式 - 显式定义 */
|
/* 浅色主题样式 - 显式定义 */
|
||||||
.widget-bot-button[data-theme="light"] {
|
.widget-bot-side-sticky[data-theme="light"] {
|
||||||
background: #3248F2;
|
background: #FFFFFF;
|
||||||
color: #FFFFFF;
|
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
||||||
box-shadow: 0px 6px 10px 0px rgba(54, 59, 76, 0.17);
|
border: 1px solid #ECEEF1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-button[data-theme="light"]:hover {
|
.widget-bot-side-sticky[data-theme="light"]:hover {
|
||||||
background: #2a3cdb;
|
background: #FFFFFF;
|
||||||
box-shadow: 0 4px 8px rgba(50, 72, 242, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-bot-button[data-theme="light"].dragging {
|
.widget-bot-side-sticky[data-theme="light"].dragging {
|
||||||
box-shadow: 0 6px 12px rgba(50, 72, 242, 0.25);
|
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"] {
|
.widget-bot-modal[data-theme="light"] {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'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 currentScript = document.currentScript || document.querySelector('script[src*="widget-bot.js"]');
|
||||||
const widgetDomain = currentScript ? new URL(currentScript.src).origin : window.location.origin;
|
const widgetDomain = currentScript ? new URL(currentScript.src).origin : window.location.origin;
|
||||||
|
|
@ -11,6 +15,13 @@
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let dragOffset = { x: 0, y: 0 };
|
let dragOffset = { x: 0, y: 0 };
|
||||||
let currentTheme = 'light'; // 默认浅色主题
|
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) {
|
function applyTheme(theme_mode) {
|
||||||
|
|
@ -60,13 +71,22 @@
|
||||||
applyTheme(widgetInfo.theme_mode);
|
applyTheme(widgetInfo.theme_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
createWidget();
|
// 根据 btn_style 创建不同的挂件
|
||||||
|
const btnStyle = widgetInfo.btn_style || defaultBtnStyle;
|
||||||
|
if (btnStyle === 'btn_trigger') {
|
||||||
|
createCustomTrigger();
|
||||||
|
} else {
|
||||||
|
createWidget();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取挂件信息失败:', error);
|
console.error('获取挂件信息失败:', error);
|
||||||
// 使用默认值
|
// 使用默认值
|
||||||
widgetInfo = {
|
widgetInfo = {
|
||||||
btn_text: '在线客服',
|
btn_text: '在线客服',
|
||||||
btn_logo: '',
|
btn_logo: `''`,
|
||||||
|
btn_style: defaultBtnStyle,
|
||||||
|
btn_position: defaultBtnPosition,
|
||||||
|
modal_position: defaultModalPosition,
|
||||||
theme_mode: 'light'
|
theme_mode: 'light'
|
||||||
};
|
};
|
||||||
applyTheme(widgetInfo.theme_mode);
|
applyTheme(widgetInfo.theme_mode);
|
||||||
|
|
@ -78,53 +98,92 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建垂直文字
|
// 创建两行文字(每行两个字)
|
||||||
function createVerticalText(text) {
|
function createTwoLineText(text) {
|
||||||
return text.split('').map((char, index) =>
|
const chars = text.split('').filter(it => !!it.trim());
|
||||||
`<span>${char}</span>`
|
const lines = [];
|
||||||
).join('');
|
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() {
|
function applyButtonPosition(button, position) {
|
||||||
// 如果已存在,先删除
|
const pos = position || defaultBtnPosition;
|
||||||
if (widgetButton) {
|
button.style.top = 'auto';
|
||||||
widgetButton.remove();
|
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 = document.createElement('div');
|
||||||
widgetButton.className = 'widget-bot-button';
|
widgetButton.className = 'widget-bot-button widget-bot-side-sticky';
|
||||||
widgetButton.setAttribute('role', 'button');
|
widgetButton.setAttribute('role', 'button');
|
||||||
widgetButton.setAttribute('tabindex', '0');
|
widgetButton.setAttribute('tabindex', '0');
|
||||||
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text}窗口`);
|
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text || '在线客服'}窗口`);
|
||||||
widgetButton.setAttribute('data-theme', currentTheme);
|
widgetButton.setAttribute('data-theme', currentTheme);
|
||||||
|
|
||||||
const buttonContent = document.createElement('div');
|
const buttonContent = document.createElement('div');
|
||||||
buttonContent.className = 'widget-bot-button-content';
|
buttonContent.className = 'widget-bot-button-content';
|
||||||
|
|
||||||
// 添加logo(如果有)
|
// 侧边吸附显示图标和文字(btn_logo 以及 btn_text)
|
||||||
if (widgetInfo.btn_logo) {
|
const icon = document.createElement('img');
|
||||||
const logo = document.createElement('img');
|
const defaultIconSrc = widgetDomain + '/favicon.png';
|
||||||
logo.src = widgetDomain + widgetInfo.btn_logo;
|
icon.src = widgetInfo.btn_logo ? (widgetDomain + widgetInfo.btn_logo) : defaultIconSrc;
|
||||||
logo.alt = 'logo';
|
icon.alt = 'icon';
|
||||||
logo.className = 'widget-bot-logo';
|
icon.className = 'widget-bot-icon';
|
||||||
logo.onerror = () => {
|
icon.onerror = () => {
|
||||||
logo.style.display = 'none';
|
// 如果当前不是 favicon.png,尝试使用 favicon.png 作为备用
|
||||||
};
|
if (icon.src !== defaultIconSrc) {
|
||||||
buttonContent.appendChild(logo);
|
icon.src = defaultIconSrc;
|
||||||
}
|
} else {
|
||||||
|
// 如果 favicon.png 也加载失败,隐藏图标
|
||||||
|
icon.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
buttonContent.appendChild(icon);
|
||||||
|
|
||||||
// 添加文字
|
// 添加文字
|
||||||
const textDiv = document.createElement('div');
|
const textDiv = document.createElement('div');
|
||||||
textDiv.className = 'widget-bot-text';
|
textDiv.className = 'widget-bot-text';
|
||||||
textDiv.innerHTML = createVerticalText(widgetInfo.btn_text || '在线客服');
|
textDiv.innerHTML = createTwoLineText(widgetInfo.btn_text || '在线客服');
|
||||||
buttonContent.appendChild(textDiv);
|
buttonContent.appendChild(textDiv);
|
||||||
|
|
||||||
widgetButton.appendChild(buttonContent);
|
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('mousedown', startDrag);
|
||||||
widgetButton.addEventListener('keydown', handleKeyDown);
|
widgetButton.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
|
@ -134,6 +193,69 @@
|
||||||
widgetButton.addEventListener('touchend', handleTouchEnd);
|
widgetButton.addEventListener('touchend', handleTouchEnd);
|
||||||
|
|
||||||
document.body.appendChild(widgetButton);
|
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();
|
createModal();
|
||||||
|
|
@ -145,6 +267,109 @@
|
||||||
}, 100);
|
}, 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) {
|
function handleKeyDown(e) {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
|
@ -176,7 +401,8 @@
|
||||||
Math.pow(touch.clientY - touchStartPos.y, 2)
|
Math.pow(touch.clientY - touchStartPos.y, 2)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (distance < 10) {
|
// 只有在没有拖拽且移动距离很小的情况下才认为是点击
|
||||||
|
if (!hasDragged && distance < 10) {
|
||||||
// 判断为点击事件
|
// 判断为点击事件
|
||||||
setTimeout(() => showModal(), 100);
|
setTimeout(() => showModal(), 100);
|
||||||
}
|
}
|
||||||
|
|
@ -198,22 +424,41 @@
|
||||||
widgetModal.setAttribute('aria-labelledby', 'widget-modal-title');
|
widgetModal.setAttribute('aria-labelledby', 'widget-modal-title');
|
||||||
widgetModal.setAttribute('data-theme', currentTheme);
|
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');
|
const modalContent = document.createElement('div');
|
||||||
modalContent.className = 'widget-bot-modal-content';
|
modalContent.className = 'widget-bot-modal-content';
|
||||||
|
if (modalPosition === 'fixed') {
|
||||||
|
modalContent.classList.add('widget-bot-modal-content-fixed');
|
||||||
|
}
|
||||||
|
|
||||||
// 创建关闭按钮
|
// 创建关闭按钮(透明框)
|
||||||
const closeBtn = document.createElement('button');
|
const closeBtn = document.createElement('button');
|
||||||
closeBtn.className = 'widget-bot-close-btn';
|
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('aria-label', '关闭窗口');
|
||||||
closeBtn.setAttribute('type', 'button');
|
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
|
// 创建iframe
|
||||||
const iframe = document.createElement('iframe');
|
const iframe = document.createElement('iframe');
|
||||||
iframe.className = 'widget-bot-iframe';
|
iframe.className = 'widget-bot-iframe';
|
||||||
iframe.src = `${widgetDomain}/widget`;
|
iframe.src = `${widgetDomain}/widget`;
|
||||||
iframe.setAttribute('title', `${widgetInfo.btn_text}服务窗口`);
|
iframe.setAttribute('title', `${widgetInfo.btn_text || '在线客服'}服务窗口`);
|
||||||
iframe.setAttribute('allow', 'camera; microphone; geolocation');
|
iframe.setAttribute('allow', 'camera; microphone; geolocation');
|
||||||
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-forms allow-popups allow-presentation');
|
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-forms allow-popups allow-presentation');
|
||||||
|
|
||||||
|
|
@ -224,6 +469,156 @@
|
||||||
document.body.appendChild(widgetModal);
|
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() {
|
function showModal() {
|
||||||
if (!widgetModal) return;
|
if (!widgetModal) return;
|
||||||
|
|
@ -231,27 +626,31 @@
|
||||||
widgetModal.style.display = 'flex';
|
widgetModal.style.display = 'flex';
|
||||||
document.body.classList.add('widget-bot-modal-open');
|
document.body.classList.add('widget-bot-modal-open');
|
||||||
|
|
||||||
// 计算模态框位置
|
const modalPosition = widgetInfo.modal_position || defaultModalPosition;
|
||||||
requestAnimationFrame(() => {
|
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||||
const buttonRect = widgetButton.getBoundingClientRect();
|
|
||||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
|
||||||
|
|
||||||
if (modalContent) {
|
// 移动端强制居中显示
|
||||||
// 设置模态框位置:距离按钮16px,距离底部24px
|
if (isMobile()) {
|
||||||
const modalBottom = 24;
|
modalContent.style.position = 'relative';
|
||||||
const modalRight = Math.max(16, window.innerWidth - buttonRect.left + 16);
|
modalContent.style.top = 'auto';
|
||||||
|
modalContent.style.left = 'auto';
|
||||||
modalContent.style.bottom = modalBottom + 'px';
|
modalContent.style.right = 'auto';
|
||||||
modalContent.style.right = modalRight + 'px';
|
modalContent.style.bottom = 'auto';
|
||||||
|
modalContent.style.margin = 'auto';
|
||||||
// 确保模态框不会超出屏幕
|
modalContent.style.width = 'calc(100% - 32px)';
|
||||||
const modalRect = modalContent.getBoundingClientRect();
|
modalContent.style.height = 'auto';
|
||||||
if (modalRect.left < 16) {
|
} else if (modalPosition === 'fixed') {
|
||||||
modalContent.style.right = '16px';
|
// 桌面端固定模式:居中展示
|
||||||
modalContent.style.left = '16px';
|
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键关闭功能
|
// 添加ESC键关闭功能
|
||||||
document.addEventListener('keydown', handleEscKey);
|
document.addEventListener('keydown', handleEscKey);
|
||||||
|
|
@ -287,42 +686,98 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
|
hasDragged = false; // 重置拖拽标记
|
||||||
|
|
||||||
const rect = widgetButton.getBoundingClientRect();
|
const rect = widgetButton.getBoundingClientRect();
|
||||||
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
|
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
|
||||||
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
|
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';
|
buttonSize.width = rect.width;
|
||||||
widgetButton.style.top = rect.top + 'px';
|
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.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);
|
document.addEventListener('mouseup', stopDrag);
|
||||||
|
|
||||||
widgetButton.classList.add('dragging');
|
widgetButton.classList.add('dragging');
|
||||||
widgetButton.style.zIndex = '10001';
|
widgetButton.style.zIndex = '10001';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 拖拽中
|
// 拖拽中 - 直接更新位置,实现丝滑跟随
|
||||||
function drag(e) {
|
function drag(e) {
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
|
|
||||||
if (e.preventDefault) {
|
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);
|
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 newTop = clientY - dragOffset.y;
|
||||||
const maxTop = window.innerHeight - widgetButton.offsetHeight;
|
|
||||||
|
|
||||||
// 限制在屏幕范围内
|
// 垂直位置:限制在屏幕范围内,距离顶部和底部最小距离为 24px
|
||||||
const constrainedTop = Math.max(0, Math.min(newTop, maxTop));
|
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.top = constrainedTop + 'px';
|
||||||
|
widgetButton.style.right = 'auto';
|
||||||
|
widgetButton.style.bottom = 'auto';
|
||||||
|
widgetButton.style.transform = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止拖拽
|
// 停止拖拽
|
||||||
|
|
@ -330,26 +785,75 @@
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
|
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
|
|
||||||
|
// 取消待执行的动画帧
|
||||||
|
if (dragAnimationFrame) {
|
||||||
|
cancelAnimationFrame(dragAnimationFrame);
|
||||||
|
dragAnimationFrame = null;
|
||||||
|
}
|
||||||
|
|
||||||
document.removeEventListener('mousemove', drag);
|
document.removeEventListener('mousemove', drag);
|
||||||
document.removeEventListener('mouseup', stopDrag);
|
document.removeEventListener('mouseup', stopDrag);
|
||||||
|
|
||||||
widgetButton.classList.remove('dragging');
|
widgetButton.classList.remove('dragging');
|
||||||
widgetButton.style.zIndex = '9999';
|
widgetButton.style.zIndex = '9999';
|
||||||
|
|
||||||
// 吸附到右侧,恢复bottom定位
|
// 恢复过渡效果
|
||||||
|
widgetButton.style.transition = '';
|
||||||
|
widgetButton.style.willChange = '';
|
||||||
|
|
||||||
|
// 根据按钮类型和当前位置进行最终定位
|
||||||
requestAnimationFrame(() => {
|
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 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
|
// 垂直位置:保持在当前位置,限制在屏幕范围内,距离顶部和底部最小距离为 24px
|
||||||
widgetButton.style.right = '0';
|
const minTop = 24;
|
||||||
widgetButton.style.bottom = Math.max(20, bottomPosition) + 'px';
|
const maxTop = Math.max(minTop, windowHeight - buttonHeight - 24);
|
||||||
widgetButton.style.top = 'auto';
|
const finalTop = Math.max(minTop, Math.min(currentTop, maxTop));
|
||||||
widgetButton.style.left = 'auto';
|
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 () {
|
window.addEventListener('resize', function () {
|
||||||
if (widgetModal && widgetModal.style.display === 'flex') {
|
if (widgetModal && widgetModal.style.display === 'flex') {
|
||||||
// 重新计算模态框位置
|
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||||
setTimeout(() => {
|
if (!modalContent) return;
|
||||||
const buttonRect = widgetButton.getBoundingClientRect();
|
|
||||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
|
||||||
|
|
||||||
if (modalContent) {
|
// 移动端强制居中显示
|
||||||
const modalBottom = 24;
|
if (isMobile()) {
|
||||||
const modalRight = Math.max(16, window.innerWidth - buttonRect.left + 16);
|
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';
|
const modalPosition = widgetInfo?.modal_position || defaultModalPosition;
|
||||||
modalContent.style.right = modalRight + 'px';
|
if (modalPosition === 'fixed') {
|
||||||
}
|
// 固定居中模式不需要重新定位
|
||||||
}, 100);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新计算模态框位置(使用智能定位)
|
||||||
|
positionModalFollow(modalContent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -423,8 +938,13 @@
|
||||||
if (widgetModal) {
|
if (widgetModal) {
|
||||||
widgetModal.remove();
|
widgetModal.remove();
|
||||||
}
|
}
|
||||||
|
if (customTriggerElement && customTriggerHandler) {
|
||||||
|
customTriggerElement.removeEventListener('click', customTriggerHandler);
|
||||||
|
customTriggerElement.removeAttribute('data-widget-trigger-attached');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 启动
|
// 启动
|
||||||
init();
|
init();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { ThemeStoreProvider } from '@/provider/themeStore';
|
||||||
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
||||||
import { getShareProV1AuthInfo } from '@/request/pro/ShareAuth';
|
import { getShareProV1AuthInfo } from '@/request/pro/ShareAuth';
|
||||||
import { Box } from '@mui/material';
|
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 type { Metadata, Viewport } from 'next';
|
||||||
import localFont from 'next/font/local';
|
import localFont from 'next/font/local';
|
||||||
import { headers, cookies } from 'next/headers';
|
import { headers, cookies } from 'next/headers';
|
||||||
|
|
@ -92,7 +92,7 @@ const Layout = async ({
|
||||||
return (
|
return (
|
||||||
<html lang='en'>
|
<html lang='en'>
|
||||||
<body
|
<body
|
||||||
className={`${gilory.variable} ${themeMode === 'dark' ? 'dark' : ''}`}
|
className={`${gilory.variable} ${themeMode === 'dark' ? 'dark' : 'light'}`}
|
||||||
>
|
>
|
||||||
<AppRouterCacheProvider>
|
<AppRouterCacheProvider>
|
||||||
<ThemeStoreProvider themeMode={themeMode}>
|
<ThemeStoreProvider themeMode={themeMode}>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
--color-primary-main: #6e73fe;
|
--color-primary-main: #6e73fe;
|
||||||
|
|
||||||
/* 代码块颜色 */
|
/* 代码块颜色 */
|
||||||
--code-bg: #ffffff;
|
--code-bg: rgba(0, 0, 0, 0.03);
|
||||||
--code-color: #21222d;
|
--code-color: #21222d;
|
||||||
--inline-code-bg: #fff5f5;
|
--inline-code-bg: #fff5f5;
|
||||||
--inline-code-color: #ff502c;
|
--inline-code-color: #ff502c;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import StoreProvider from '@/provider';
|
import StoreProvider from '@/provider';
|
||||||
import { darkThemeWidget, lightThemeWidget } from '@/theme';
|
|
||||||
import { getShareV1AppWidgetInfo } from '@/request/ShareApp';
|
import { getShareV1AppWidgetInfo } from '@/request/ShareApp';
|
||||||
import { ThemeProvider } from '@ctzhian/ui';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Layout = async ({
|
const Layout = async ({
|
||||||
|
|
@ -12,18 +9,7 @@ const Layout = async ({
|
||||||
}>) => {
|
}>) => {
|
||||||
const widgetDetail: any = await getShareV1AppWidgetInfo();
|
const widgetDetail: any = await getShareV1AppWidgetInfo();
|
||||||
|
|
||||||
const themeMode =
|
return <StoreProvider widget={widgetDetail}>{children}</StoreProvider>;
|
||||||
widgetDetail?.settings?.widget_bot_settings?.theme_mode || 'light';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeProvider
|
|
||||||
theme={themeMode === 'dark' ? darkThemeWidget : lightThemeWidget}
|
|
||||||
>
|
|
||||||
<StoreProvider widget={widgetDetail} themeMode={themeMode || 'light'}>
|
|
||||||
{children}
|
|
||||||
</StoreProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Layout;
|
export default Layout;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,3 @@
|
||||||
import Widget from '@/views/widget';
|
import Widget from '@/views/widget';
|
||||||
import { Box } from '@mui/material';
|
|
||||||
|
|
||||||
const Page = () => {
|
export default Widget;
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: '100vw',
|
|
||||||
height: '100vh',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Widget />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
|
|
|
||||||
|
|
@ -109,10 +109,19 @@ export type WidgetInfo = {
|
||||||
search_placeholder: string;
|
search_placeholder: string;
|
||||||
recommend_questions: string[];
|
recommend_questions: string[];
|
||||||
widget_bot_settings: {
|
widget_bot_settings: {
|
||||||
btn_logo: string;
|
btn_logo?: string;
|
||||||
btn_text: string;
|
btn_text?: string;
|
||||||
is_open: boolean;
|
btn_style?: string;
|
||||||
theme_mode: 'light' | 'dark';
|
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;
|
message_id: string;
|
||||||
source: 'history' | 'chat';
|
source: 'history' | 'chat';
|
||||||
chunk_result: ChunkResultItem[];
|
chunk_result: ChunkResultItem[];
|
||||||
|
result_expend: boolean;
|
||||||
|
thinking_expend: boolean;
|
||||||
thinking_content: string;
|
thinking_content: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
@ -382,6 +384,8 @@ const AiQaContent: React.FC<{
|
||||||
const solution = await cap.solve();
|
const solution = await cap.solve();
|
||||||
token = solution.token;
|
token = solution.token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
setThinking(4);
|
||||||
message.error('验证失败');
|
message.error('验证失败');
|
||||||
console.log(error, 'error---------');
|
console.log(error, 'error---------');
|
||||||
return;
|
return;
|
||||||
|
|
@ -465,6 +469,8 @@ const AiQaContent: React.FC<{
|
||||||
if (lastConversation) {
|
if (lastConversation) {
|
||||||
lastConversation.a = answerContent;
|
lastConversation.a = answerContent;
|
||||||
lastConversation.thinking_content = thinkingContent;
|
lastConversation.thinking_content = thinkingContent;
|
||||||
|
lastConversation.result_expend = false;
|
||||||
|
lastConversation.thinking_expend = false;
|
||||||
}
|
}
|
||||||
return newConversation;
|
return newConversation;
|
||||||
});
|
});
|
||||||
|
|
@ -513,6 +519,8 @@ const AiQaContent: React.FC<{
|
||||||
source: 'chat',
|
source: 'chat',
|
||||||
chunk_result: [],
|
chunk_result: [],
|
||||||
thinking_content: '',
|
thinking_content: '',
|
||||||
|
result_expend: true,
|
||||||
|
thinking_expend: true,
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
});
|
});
|
||||||
messageIdRef.current = '';
|
messageIdRef.current = '';
|
||||||
|
|
@ -631,6 +639,8 @@ const AiQaContent: React.FC<{
|
||||||
source: 'history',
|
source: 'history',
|
||||||
chunk_result: [],
|
chunk_result: [],
|
||||||
thinking_content: '',
|
thinking_content: '',
|
||||||
|
result_expend: true,
|
||||||
|
thinking_expend: true,
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -667,6 +677,8 @@ const AiQaContent: React.FC<{
|
||||||
chunk_result: [],
|
chunk_result: [],
|
||||||
thinking_content: '',
|
thinking_content: '',
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
|
result_expend: true,
|
||||||
|
thinking_expend: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -791,7 +803,16 @@ const AiQaContent: React.FC<{
|
||||||
<StyledAiBubble>
|
<StyledAiBubble>
|
||||||
{/* 搜索结果 */}
|
{/* 搜索结果 */}
|
||||||
{item.chunk_result.length > 0 && (
|
{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
|
<StyledChunkAccordionSummary
|
||||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||||
>
|
>
|
||||||
|
|
@ -837,7 +858,16 @@ const AiQaContent: React.FC<{
|
||||||
|
|
||||||
{/* 思考过程 */}
|
{/* 思考过程 */}
|
||||||
{!!item.thinking_content && (
|
{!!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
|
<StyledThinkingAccordionSummary
|
||||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||||
>
|
>
|
||||||
|
|
@ -929,6 +959,9 @@ const AiQaContent: React.FC<{
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Box>
|
||||||
|
{kbDetail?.settings?.disclaimer_settings?.content}
|
||||||
|
</Box>
|
||||||
</StyledActionStack>
|
</StyledActionStack>
|
||||||
)}
|
)}
|
||||||
</StyledAiBubble>
|
</StyledAiBubble>
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,9 @@ export const StyledUserBubble = styled(Box)(({ theme }) => ({
|
||||||
|
|
||||||
export const StyledAiBubble = styled(Box)(({ theme }) => ({
|
export const StyledAiBubble = styled(Box)(({ theme }) => ({
|
||||||
alignSelf: 'flex-start',
|
alignSelf: 'flex-start',
|
||||||
maxWidth: '85%',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
gap: theme.spacing(3),
|
gap: theme.spacing(3),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,9 @@ const QaModal: React.FC<QaModalProps> = () => {
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
px: 3,
|
px: 3,
|
||||||
pt: kbDetail?.settings?.disclaimer_settings?.content ? 2 : 0,
|
pt: kbDetail?.settings?.web_app_custom_style?.show_brand_info
|
||||||
|
? 2
|
||||||
|
: 0,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
@ -263,7 +265,10 @@ const QaModal: React.FC<QaModalProps> = () => {
|
||||||
gap: 1,
|
gap: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>{kbDetail?.settings?.disclaimer_settings?.content}</Box>
|
<Box>
|
||||||
|
{kbDetail?.settings?.web_app_custom_style?.show_brand_info &&
|
||||||
|
'本网站由 PandaWiki 提供技术支持'}
|
||||||
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Logo from '@/assets/images/logo.png';
|
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 { useStore } from '@/provider';
|
||||||
import { usePathname } from 'next/navigation';
|
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 {
|
import {
|
||||||
Header as CustomHeader,
|
Header as CustomHeader,
|
||||||
WelcomeHeader as WelcomeHeaderComponent,
|
WelcomeHeader as WelcomeHeaderComponent,
|
||||||
|
|
@ -16,8 +20,58 @@ interface HeaderProps {
|
||||||
isWelcomePage?: boolean;
|
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 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 pathname = usePathname();
|
||||||
const docWidth = useMemo(() => {
|
const docWidth = useMemo(() => {
|
||||||
if (isWelcomePage) return 'full';
|
if (isWelcomePage) return 'full';
|
||||||
|
|
@ -55,16 +109,23 @@ const Header = ({ isDocPage = false, isWelcomePage = false }: HeaderProps) => {
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onQaClick={() => setQaModalOpen?.(true)}
|
onQaClick={() => setQaModalOpen?.(true)}
|
||||||
>
|
>
|
||||||
<Box sx={{ ml: 2 }}>
|
<Stack sx={{ ml: 2 }} direction='row' alignItems='center' gap={1}>
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
</Box>
|
{!!authInfo && <LogoutButton />}
|
||||||
|
</Stack>
|
||||||
<QaModal />
|
<QaModal />
|
||||||
</CustomHeader>
|
</CustomHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WelcomeHeader = () => {
|
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') => {
|
const handleSearch = (value?: string, type: 'chat' | 'search' = 'chat') => {
|
||||||
if (value?.trim()) {
|
if (value?.trim()) {
|
||||||
if (type === 'chat') {
|
if (type === 'chat') {
|
||||||
|
|
@ -91,6 +152,7 @@ export const WelcomeHeader = () => {
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onQaClick={() => setQaModalOpen?.(true)}
|
onQaClick={() => setQaModalOpen?.(true)}
|
||||||
>
|
>
|
||||||
|
<Box sx={{ ml: 2 }}>{!!authInfo && <LogoutButton />}</Box>
|
||||||
<QaModal />
|
<QaModal />
|
||||||
</WelcomeHeaderComponent>
|
</WelcomeHeaderComponent>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import React, { useState, useEffect } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { styled, SvgIcon, SvgIconProps } from '@mui/material';
|
import { styled, SvgIcon, SvgIconProps } from '@mui/material';
|
||||||
|
|
||||||
// ==================== 图片数据缓存 ====================
|
// ==================== 图片数据缓存工具函数 ====================
|
||||||
// 全局图片 blob URL 缓存,避免重复请求 OSS
|
|
||||||
const imageBlobCache = new Map<string, string>();
|
|
||||||
|
|
||||||
// 下载图片并转换为 blob URL
|
// 下载图片并转换为 blob URL
|
||||||
const fetchImageAsBlob = async (src: string): Promise<string> => {
|
const fetchImageAsBlob = async (
|
||||||
|
src: string,
|
||||||
|
imageBlobCache: Map<string, string>,
|
||||||
|
): Promise<string> => {
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
if (imageBlobCache.has(src)) {
|
if (imageBlobCache.has(src)) {
|
||||||
return imageBlobCache.get(src)!;
|
return imageBlobCache.get(src)!;
|
||||||
|
|
@ -39,12 +39,8 @@ const fetchImageAsBlob = async (src: string): Promise<string> => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 导出获取图片 blob URL 的函数
|
// 清理图片 blob 缓存
|
||||||
export const getImageBlobUrl = (src: string): string | null => {
|
export const clearImageBlobCache = (imageBlobCache: Map<string, string>) => {
|
||||||
return imageBlobCache.get(src) || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearImageBlobCache = () => {
|
|
||||||
imageBlobCache.forEach(url => {
|
imageBlobCache.forEach(url => {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
});
|
});
|
||||||
|
|
@ -54,7 +50,7 @@ export const clearImageBlobCache = () => {
|
||||||
const StyledErrorContainer = styled('div')(({ theme }) => ({
|
const StyledErrorContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(1, 6),
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
|
|
@ -71,7 +67,7 @@ const StyledErrorContainer = styled('div')(({ theme }) => ({
|
||||||
|
|
||||||
const StyledErrorText = styled('div')(() => ({
|
const StyledErrorText = styled('div')(() => ({
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
marginBottom: 16,
|
marginBottom: 10,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ImageErrorIcon = (props: SvgIconProps) => {
|
export const ImageErrorIcon = (props: SvgIconProps) => {
|
||||||
|
|
@ -102,7 +98,7 @@ export const ImageErrorIcon = (props: SvgIconProps) => {
|
||||||
const ImageErrorDisplay: React.FC = () => (
|
const ImageErrorDisplay: React.FC = () => (
|
||||||
<StyledErrorContainer>
|
<StyledErrorContainer>
|
||||||
<ImageErrorIcon
|
<ImageErrorIcon
|
||||||
sx={{ color: 'var(--mui-palette-text-tertiary)', fontSize: 160 }}
|
sx={{ color: 'var(--mui-palette-text-tertiary)', fontSize: 140 }}
|
||||||
/>
|
/>
|
||||||
<StyledErrorText>图片加载失败</StyledErrorText>
|
<StyledErrorText>图片加载失败</StyledErrorText>
|
||||||
</StyledErrorContainer>
|
</StyledErrorContainer>
|
||||||
|
|
@ -116,7 +112,7 @@ interface ImageComponentProps {
|
||||||
imageIndex: number;
|
imageIndex: number;
|
||||||
onLoad: (index: number, html: string) => void;
|
onLoad: (index: number, html: string) => void;
|
||||||
onError: (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,
|
imageIndex,
|
||||||
onLoad,
|
onLoad,
|
||||||
onError,
|
onError,
|
||||||
onImageClick,
|
imageBlobCache,
|
||||||
}) => {
|
}) => {
|
||||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>(
|
const [status, setStatus] = useState<'loading' | 'success' | 'error'>(
|
||||||
'loading',
|
'loading',
|
||||||
|
|
@ -149,7 +145,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
||||||
// 获取图片 blob URL
|
// 获取图片 blob URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
fetchImageAsBlob(src)
|
fetchImageAsBlob(src, imageBlobCache)
|
||||||
.then(url => {
|
.then(url => {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setBlobUrl(url);
|
setBlobUrl(url);
|
||||||
|
|
@ -166,7 +162,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
};
|
};
|
||||||
}, [src]);
|
}, [src, imageBlobCache]);
|
||||||
|
|
||||||
// 解析自定义样式
|
// 解析自定义样式
|
||||||
const parseStyleString = (styleStr: string) => {
|
const parseStyleString = (styleStr: string) => {
|
||||||
|
|
@ -238,7 +234,8 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
||||||
referrerPolicy='no-referrer'
|
referrerPolicy='no-referrer'
|
||||||
onLoad={handleLoad}
|
onLoad={handleLoad}
|
||||||
onError={handleError}
|
onError={handleError}
|
||||||
onClick={() => onImageClick(src)} // 传递原始 src 用于预览
|
data-original-src={src}
|
||||||
|
className='markdown-image'
|
||||||
{...getOtherProps()}
|
{...getOtherProps()}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -264,12 +261,13 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
||||||
export interface ImageRendererOptions {
|
export interface ImageRendererOptions {
|
||||||
onImageLoad: (index: number, html: string) => void;
|
onImageLoad: (index: number, html: string) => void;
|
||||||
onImageError: (index: number, html: string) => void;
|
onImageError: (index: number, html: string) => void;
|
||||||
onImageClick: (src: string) => void;
|
|
||||||
imageRenderCache: Map<number, string>;
|
imageRenderCache: Map<number, string>;
|
||||||
|
imageBlobCache: Map<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createImageRenderer = (options: ImageRendererOptions) => {
|
export const createImageRenderer = (options: ImageRendererOptions) => {
|
||||||
const { onImageLoad, onImageError, imageRenderCache, onImageClick } = options;
|
const { onImageLoad, onImageError, imageRenderCache, imageBlobCache } =
|
||||||
|
options;
|
||||||
return (
|
return (
|
||||||
src: string,
|
src: string,
|
||||||
alt: string,
|
alt: string,
|
||||||
|
|
@ -279,29 +277,6 @@ export const createImageRenderer = (options: ImageRendererOptions) => {
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
const cached = imageRenderCache.get(imageIndex);
|
const cached = imageRenderCache.get(imageIndex);
|
||||||
if (cached) {
|
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;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,7 +298,7 @@ export const createImageRenderer = (options: ImageRendererOptions) => {
|
||||||
imageIndex={imageIndex}
|
imageIndex={imageIndex}
|
||||||
onLoad={onImageLoad}
|
onLoad={onImageLoad}
|
||||||
onError={onImageError}
|
onError={onImageError}
|
||||||
onImageClick={onImageClick}
|
imageBlobCache={imageBlobCache}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,7 @@ import React, {
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useSmartScroll } from '@/hooks';
|
import { useSmartScroll } from '@/hooks';
|
||||||
import {
|
import { clearImageBlobCache, createImageRenderer } from './imageRenderer';
|
||||||
clearImageBlobCache,
|
|
||||||
createImageRenderer,
|
|
||||||
getImageBlobUrl,
|
|
||||||
} from './imageRenderer';
|
|
||||||
import { incrementalRender } from './incrementalRenderer';
|
import { incrementalRender } from './incrementalRenderer';
|
||||||
import { createMermaidRenderer } from './mermaidRenderer';
|
import { createMermaidRenderer } from './mermaidRenderer';
|
||||||
import {
|
import {
|
||||||
|
|
@ -88,7 +84,8 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
const lastContentRef = useRef<string>('');
|
const lastContentRef = useRef<string>('');
|
||||||
const mdRef = useRef<MarkdownIt | null>(null);
|
const mdRef = useRef<MarkdownIt | null>(null);
|
||||||
const mermaidSuccessIdRef = useRef<Map<number, string>>(new Map());
|
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
|
// 使用智能滚动 hook
|
||||||
const { scrollToBottom } = useSmartScroll({
|
const { scrollToBottom } = useSmartScroll({
|
||||||
|
|
@ -125,13 +122,8 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
createImageRenderer({
|
createImageRenderer({
|
||||||
onImageLoad: handleImageLoad,
|
onImageLoad: handleImageLoad,
|
||||||
onImageError: handleImageError,
|
onImageError: handleImageError,
|
||||||
onImageClick: (src: string) => {
|
|
||||||
// 尝试获取缓存的 blob URL,如果不存在则使用原始 src
|
|
||||||
const blobUrl = getImageBlobUrl(src);
|
|
||||||
setPreviewImgBlobUrl(blobUrl || src);
|
|
||||||
setPreviewOpen(true);
|
|
||||||
},
|
|
||||||
imageRenderCache: imageRenderCacheRef.current,
|
imageRenderCache: imageRenderCacheRef.current,
|
||||||
|
imageBlobCache: imageBlobCacheRef.current,
|
||||||
}),
|
}),
|
||||||
[handleImageLoad, handleImageError],
|
[handleImageLoad, handleImageError],
|
||||||
);
|
);
|
||||||
|
|
@ -158,6 +150,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
const originalFenceRender = md.renderer.rules.fence;
|
const originalFenceRender = md.renderer.rules.fence;
|
||||||
// 自定义图片渲染
|
// 自定义图片渲染
|
||||||
let imageCount = 0;
|
let imageCount = 0;
|
||||||
|
let htmlImageCount = 0; // HTML 标签图片计数
|
||||||
let mermaidCount = 0;
|
let mermaidCount = 0;
|
||||||
md.renderer.rules.image = (tokens, idx) => {
|
md.renderer.rules.image = (tokens, idx) => {
|
||||||
imageCount++;
|
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 = (
|
md.renderer.rules.html_block = (
|
||||||
tokens,
|
tokens,
|
||||||
idx,
|
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 class="chat-error">';
|
||||||
if (content.includes('</error>')) return '</span>';
|
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)) {
|
if (!isAllowedTag(content)) {
|
||||||
return md.utils.escapeHtml(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 class="chat-error">';
|
||||||
if (content.includes('</error>')) return '</span>';
|
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)) {
|
if (!isAllowedTag(content)) {
|
||||||
return md.utils.escapeHtml(content);
|
return md.utils.escapeHtml(content);
|
||||||
|
|
@ -352,7 +407,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
}
|
}
|
||||||
}, [content, customizeRenderer, scrollToBottom]);
|
}, [content, customizeRenderer, scrollToBottom]);
|
||||||
|
|
||||||
// 添加代码块点击复制功能
|
// 添加代码块点击复制和图片点击预览功能(事件代理)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
@ -360,6 +415,21 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
const target = e.target as HTMLElement;
|
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');
|
const preElement = target.closest('pre.hljs');
|
||||||
if (preElement) {
|
if (preElement) {
|
||||||
|
|
@ -368,6 +438,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
const code = codeElement.textContent || '';
|
const code = codeElement.textContent || '';
|
||||||
copyText(code.replace(/\n$/, ''));
|
copyText(code.replace(/\n$/, ''));
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否点击了行内代码
|
// 检查是否点击了行内代码
|
||||||
|
|
@ -380,7 +451,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
container.addEventListener('click', handleClick);
|
container.addEventListener('click', handleClick);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearImageBlobCache();
|
clearImageBlobCache(imageBlobCacheRef.current);
|
||||||
container.removeEventListener('click', handleClick);
|
container.removeEventListener('click', handleClick);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -406,6 +477,9 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
},
|
},
|
||||||
|
'.markdown-image': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
'.image-error': {
|
'.image-error': {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
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(() => {
|
useEffect(() => {
|
||||||
Cookies.set('theme_mode', themeMode, { expires: 365 * 10 });
|
Cookies.set('theme_mode', themeMode, { expires: 365 * 10 });
|
||||||
}, [themeMode]);
|
}, [themeMode]);
|
||||||
|
|
||||||
|
console.log('themeMode-------', themeMode);
|
||||||
|
console.log('themeMode-------', theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ themeMode, setThemeMode }}>
|
<ThemeContext.Provider value={{ themeMode, setThemeMode }}>
|
||||||
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
<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,
|
DomainOpenAICompletionsResponse,
|
||||||
DomainResponse,
|
DomainResponse,
|
||||||
PostShareV1ChatMessageParams,
|
PostShareV1ChatMessageParams,
|
||||||
PostShareV1ChatWidgetParams,
|
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -92,28 +91,3 @@ export const postShareV1ChatMessage = (
|
||||||
format: "json",
|
format: "json",
|
||||||
...params,
|
...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 './ShareOpenapi'
|
||||||
export * from './ShareStat'
|
export * from './ShareStat'
|
||||||
export * from './Wechat'
|
export * from './Wechat'
|
||||||
|
export * from './Widget'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
||||||
|
GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
||||||
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
||||||
|
|
@ -206,6 +207,32 @@ export const postShareProV1AuthLdap = (
|
||||||
...params,
|
...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登录
|
* @description OAuth登录
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,12 @@ export enum ConstsSourceType {
|
||||||
export enum ConstsLicenseEdition {
|
export enum ConstsLicenseEdition {
|
||||||
/** 开源版 */
|
/** 开源版 */
|
||||||
LicenseEditionFree = 0,
|
LicenseEditionFree = 0,
|
||||||
/** 联创版 */
|
/** 专业版 */
|
||||||
LicenseEditionContributor = 1,
|
LicenseEditionProfession = 1,
|
||||||
/** 企业版 */
|
/** 企业版 */
|
||||||
LicenseEditionEnterprise = 2,
|
LicenseEditionEnterprise = 2,
|
||||||
|
/** 商业版 */
|
||||||
|
LicenseEditionBusiness = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConstsContributeType {
|
export enum ConstsContributeType {
|
||||||
|
|
@ -455,6 +457,11 @@ export type GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp = Record<
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp = Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
|
||||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
||||||
kb_id?: string;
|
kb_id?: string;
|
||||||
redirect_url?: string;
|
redirect_url?: string;
|
||||||
|
|
@ -669,8 +676,6 @@ export interface GetApiProV1TokenListParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PostApiV1LicensePayload {
|
export interface PostApiV1LicensePayload {
|
||||||
/** license edition */
|
|
||||||
license_edition: "contributor" | "enterprise";
|
|
||||||
/** license type */
|
/** license type */
|
||||||
license_type: "file" | "code";
|
license_type: "file" | "code";
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -171,14 +171,21 @@ export enum ConstsNodeAccessPerm {
|
||||||
NodeAccessPermClosed = "closed",
|
NodeAccessPermClosed = "closed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ConstsModelSettingMode {
|
||||||
|
ModelSettingModeManual = "manual",
|
||||||
|
ModelSettingModeAuto = "auto",
|
||||||
|
}
|
||||||
|
|
||||||
/** @format int32 */
|
/** @format int32 */
|
||||||
export enum ConstsLicenseEdition {
|
export enum ConstsLicenseEdition {
|
||||||
/** 开源版 */
|
/** 开源版 */
|
||||||
LicenseEditionFree = 0,
|
LicenseEditionFree = 0,
|
||||||
/** 联创版 */
|
/** 专业版 */
|
||||||
LicenseEditionContributor = 1,
|
LicenseEditionProfession = 1,
|
||||||
/** 企业版 */
|
/** 企业版 */
|
||||||
LicenseEditionEnterprise = 2,
|
LicenseEditionEnterprise = 2,
|
||||||
|
/** 商业版 */
|
||||||
|
LicenseEditionBusiness = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConstsHomePageSetting {
|
export enum ConstsHomePageSetting {
|
||||||
|
|
@ -922,6 +929,17 @@ export interface DomainMetricsConfig {
|
||||||
type?: string;
|
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 {
|
export interface DomainMoveNodeReq {
|
||||||
id: string;
|
id: string;
|
||||||
kb_id: string;
|
kb_id: string;
|
||||||
|
|
@ -1171,6 +1189,17 @@ export interface DomainShareConversationMessage {
|
||||||
role?: SchemaRoleType;
|
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 {
|
export interface DomainSimpleAuth {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
|
@ -1195,6 +1224,18 @@ export interface DomainStatPageReq {
|
||||||
scene: 1 | 2 | 3 | 4;
|
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 {
|
export interface DomainTextConfig {
|
||||||
title?: string;
|
title?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
|
@ -1336,11 +1377,18 @@ export interface DomainWecomAIBotSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DomainWidgetBotSettings {
|
export interface DomainWidgetBotSettings {
|
||||||
|
btn_id?: string;
|
||||||
btn_logo?: string;
|
btn_logo?: string;
|
||||||
|
btn_position?: string;
|
||||||
|
btn_style?: string;
|
||||||
btn_text?: string;
|
btn_text?: string;
|
||||||
|
disclaimer?: string;
|
||||||
is_open?: boolean;
|
is_open?: boolean;
|
||||||
|
modal_position?: string;
|
||||||
|
placeholder?: string;
|
||||||
recommend_node_ids?: string[];
|
recommend_node_ids?: string[];
|
||||||
recommend_questions?: string[];
|
recommend_questions?: string[];
|
||||||
|
search_mode?: string;
|
||||||
theme_mode?: string;
|
theme_mode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1646,6 +1694,7 @@ export interface V1ShareNodeDetailResp {
|
||||||
editor_id?: string;
|
editor_id?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
kb_id?: string;
|
kb_id?: string;
|
||||||
|
list?: DomainShareNodeListItemResp[];
|
||||||
meta?: DomainNodeMeta;
|
meta?: DomainNodeMeta;
|
||||||
name?: string;
|
name?: string;
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ const DocContent = ({
|
||||||
setCommentImages([]);
|
setCommentImages([]);
|
||||||
message.success(
|
message.success(
|
||||||
appDetail?.web_app_comment_settings?.moderation_enable
|
appDetail?.web_app_comment_settings?.moderation_enable
|
||||||
? '正在审核中...'
|
? '评论已提交,请耐心等待审核'
|
||||||
: '评论成功',
|
: '评论成功',
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} 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