mirror of https://github.com/chaitin/PandaWiki.git
Compare commits
25 Commits
cfc53da267
...
b8f2b95f22
| Author | SHA1 | Date |
|---|---|---|
|
|
b8f2b95f22 | |
|
|
98c602819f | |
|
|
be163a5f80 | |
|
|
4c03078103 | |
|
|
2b372fd81d | |
|
|
74c6ba131d | |
|
|
6c77246e34 | |
|
|
d347482d31 | |
|
|
92443dfafe | |
|
|
1dd2d93010 | |
|
|
54bf1fd108 | |
|
|
c23ce398d7 | |
|
|
2c90932f60 | |
|
|
035ce0284d | |
|
|
93559125c2 | |
|
|
6c5cf256ac | |
|
|
23bdfc3d50 | |
|
|
9008c537cd | |
|
|
63a4a964b1 | |
|
|
62f2b2eaf5 | |
|
|
5c1c6368b8 | |
|
|
7282503acf | |
|
|
60a4177229 | |
|
|
2f706a6100 | |
|
|
febcb06654 |
|
|
@ -7,21 +7,22 @@ import (
|
|||
)
|
||||
|
||||
type ShareNodeDetailResp struct {
|
||||
ID string `json:"id"`
|
||||
KbID string `json:"kb_id"`
|
||||
Type domain.NodeType `json:"type"`
|
||||
Status domain.NodeStatus `json:"status"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Meta domain.NodeMeta `json:"meta"`
|
||||
ParentID string `json:"parent_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Permissions domain.NodePermissions `json:"permissions"`
|
||||
CreatorId string `json:"creator_id"`
|
||||
EditorId string `json:"editor_id"`
|
||||
PublisherId string `json:"publisher_id"`
|
||||
CreatorAccount string `json:"creator_account"`
|
||||
EditorAccount string `json:"editor_account"`
|
||||
PublisherAccount string `json:"publisher_account"`
|
||||
ID string `json:"id"`
|
||||
KbID string `json:"kb_id"`
|
||||
Type domain.NodeType `json:"type"`
|
||||
Status domain.NodeStatus `json:"status"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
Meta domain.NodeMeta `json:"meta"`
|
||||
ParentID string `json:"parent_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Permissions domain.NodePermissions `json:"permissions"`
|
||||
CreatorId string `json:"creator_id"`
|
||||
EditorId string `json:"editor_id"`
|
||||
PublisherId string `json:"publisher_id"`
|
||||
CreatorAccount string `json:"creator_account"`
|
||||
EditorAccount string `json:"editor_account"`
|
||||
PublisherAccount string `json:"publisher_account"`
|
||||
List []*domain.ShareNodeListItemResp `json:"list" gorm:"-"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//go:build wireinject
|
||||
// +build wireinject
|
||||
|
||||
package main
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//go:build wireinject
|
||||
// +build wireinject
|
||||
|
||||
package main
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
//go:build wireinject
|
||||
// +build wireinject
|
||||
|
||||
package main
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package consts
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
|
|
@ -13,28 +11,13 @@ const ContextKeyEdition contextKey = "edition"
|
|||
type LicenseEdition int32
|
||||
|
||||
const (
|
||||
LicenseEditionFree LicenseEdition = 0 // 开源版
|
||||
LicenseEditionContributor LicenseEdition = 1 // 联创版
|
||||
LicenseEditionEnterprise LicenseEdition = 2 // 企业版
|
||||
LicenseEditionFree LicenseEdition = 0 // 开源版
|
||||
LicenseEditionProfession LicenseEdition = 1 // 专业版
|
||||
LicenseEditionEnterprise LicenseEdition = 2 // 企业版
|
||||
LicenseEditionBusiness LicenseEdition = 3 // 商业版
|
||||
)
|
||||
|
||||
func GetLicenseEdition(c echo.Context) LicenseEdition {
|
||||
edition, _ := c.Get("edition").(LicenseEdition)
|
||||
return edition
|
||||
}
|
||||
|
||||
func (e LicenseEdition) GetMaxAuth(sourceType SourceType) int {
|
||||
switch e {
|
||||
case LicenseEditionFree:
|
||||
if sourceType == SourceTypeGitHub {
|
||||
return 10
|
||||
}
|
||||
return 0
|
||||
case LicenseEditionContributor:
|
||||
return 10
|
||||
case LicenseEditionEnterprise:
|
||||
return math.MaxInt
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3478,7 +3478,7 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"share_chat"
|
||||
"Widget"
|
||||
],
|
||||
"summary": "ChatWidget",
|
||||
"parameters": [
|
||||
|
|
@ -3509,6 +3509,52 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/share/v1/chat/widget/search": {
|
||||
"post": {
|
||||
"description": "WidgetSearch",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Widget"
|
||||
],
|
||||
"summary": "WidgetSearch",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Comment",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChatSearchReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/domain.ChatSearchResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/share/v1/comment": {
|
||||
"post": {
|
||||
"description": "CreateComment",
|
||||
|
|
@ -4067,22 +4113,26 @@ const docTemplate = `{
|
|||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
2,
|
||||
3
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"LicenseEditionContributor": "联创版",
|
||||
"LicenseEditionBusiness": "商业版",
|
||||
"LicenseEditionEnterprise": "企业版",
|
||||
"LicenseEditionFree": "开源版"
|
||||
"LicenseEditionFree": "开源版",
|
||||
"LicenseEditionProfession": "专业版"
|
||||
},
|
||||
"x-enum-descriptions": [
|
||||
"开源版",
|
||||
"联创版",
|
||||
"企业版"
|
||||
"专业版",
|
||||
"企业版",
|
||||
"商业版"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"LicenseEditionFree",
|
||||
"LicenseEditionContributor",
|
||||
"LicenseEditionEnterprise"
|
||||
"LicenseEditionProfession",
|
||||
"LicenseEditionEnterprise",
|
||||
"LicenseEditionBusiness"
|
||||
]
|
||||
},
|
||||
"consts.ModelSettingMode": {
|
||||
|
|
@ -6311,6 +6361,9 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.MessageContent": {
|
||||
"type": "object"
|
||||
},
|
||||
"domain.MessageFrom": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
|
@ -6712,6 +6765,9 @@ const docTemplate = `{
|
|||
"stream": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"stream_options": {
|
||||
"$ref": "#/definitions/domain.OpenAIStreamOptions"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
|
|
@ -6834,7 +6890,7 @@ const docTemplate = `{
|
|||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/domain.MessageContent"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
|
|
@ -6864,6 +6920,14 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.OpenAIStreamOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"include_usage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.OpenAITool": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
@ -7130,6 +7194,35 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.ShareNodeListItemResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"emoji": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/definitions/domain.NodePermissions"
|
||||
},
|
||||
"position": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/domain.NodeType"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.SimpleAuth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -7647,15 +7740,33 @@ const docTemplate = `{
|
|||
"domain.WidgetBotSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"btn_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_logo": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_position": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_style": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_text": {
|
||||
"type": "string"
|
||||
},
|
||||
"disclaimer": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_open": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"modal_position": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"recommend_node_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
@ -7668,6 +7779,9 @@ const docTemplate = `{
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"search_mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme_mode": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
@ -8536,6 +8650,12 @@ const docTemplate = `{
|
|||
"kb_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/domain.ShareNodeListItemResp"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/definitions/domain.NodeMeta"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3471,7 +3471,7 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"share_chat"
|
||||
"Widget"
|
||||
],
|
||||
"summary": "ChatWidget",
|
||||
"parameters": [
|
||||
|
|
@ -3502,6 +3502,52 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/share/v1/chat/widget/search": {
|
||||
"post": {
|
||||
"description": "WidgetSearch",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Widget"
|
||||
],
|
||||
"summary": "WidgetSearch",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Comment",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/domain.ChatSearchReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/domain.Response"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/domain.ChatSearchResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/share/v1/comment": {
|
||||
"post": {
|
||||
"description": "CreateComment",
|
||||
|
|
@ -4060,22 +4106,26 @@
|
|||
"enum": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
2,
|
||||
3
|
||||
],
|
||||
"x-enum-comments": {
|
||||
"LicenseEditionContributor": "联创版",
|
||||
"LicenseEditionBusiness": "商业版",
|
||||
"LicenseEditionEnterprise": "企业版",
|
||||
"LicenseEditionFree": "开源版"
|
||||
"LicenseEditionFree": "开源版",
|
||||
"LicenseEditionProfession": "专业版"
|
||||
},
|
||||
"x-enum-descriptions": [
|
||||
"开源版",
|
||||
"联创版",
|
||||
"企业版"
|
||||
"专业版",
|
||||
"企业版",
|
||||
"商业版"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"LicenseEditionFree",
|
||||
"LicenseEditionContributor",
|
||||
"LicenseEditionEnterprise"
|
||||
"LicenseEditionProfession",
|
||||
"LicenseEditionEnterprise",
|
||||
"LicenseEditionBusiness"
|
||||
]
|
||||
},
|
||||
"consts.ModelSettingMode": {
|
||||
|
|
@ -6304,6 +6354,9 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.MessageContent": {
|
||||
"type": "object"
|
||||
},
|
||||
"domain.MessageFrom": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
|
@ -6705,6 +6758,9 @@
|
|||
"stream": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"stream_options": {
|
||||
"$ref": "#/definitions/domain.OpenAIStreamOptions"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
|
|
@ -6827,7 +6883,7 @@
|
|||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/domain.MessageContent"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
|
|
@ -6857,6 +6913,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.OpenAIStreamOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"include_usage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.OpenAITool": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
@ -7123,6 +7187,35 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"domain.ShareNodeListItemResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"emoji": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/definitions/domain.NodePermissions"
|
||||
},
|
||||
"position": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/domain.NodeType"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"domain.SimpleAuth": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -7640,15 +7733,33 @@
|
|||
"domain.WidgetBotSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"btn_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_logo": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_position": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_style": {
|
||||
"type": "string"
|
||||
},
|
||||
"btn_text": {
|
||||
"type": "string"
|
||||
},
|
||||
"disclaimer": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_open": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"modal_position": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"recommend_node_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
@ -7661,6 +7772,9 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"search_mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme_mode": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
@ -8529,6 +8643,12 @@
|
|||
"kb_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"list": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/domain.ShareNodeListItemResp"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/definitions/domain.NodeMeta"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -117,20 +117,24 @@ definitions:
|
|||
- 0
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
format: int32
|
||||
type: integer
|
||||
x-enum-comments:
|
||||
LicenseEditionContributor: 联创版
|
||||
LicenseEditionBusiness: 商业版
|
||||
LicenseEditionEnterprise: 企业版
|
||||
LicenseEditionFree: 开源版
|
||||
LicenseEditionProfession: 专业版
|
||||
x-enum-descriptions:
|
||||
- 开源版
|
||||
- 联创版
|
||||
- 专业版
|
||||
- 企业版
|
||||
- 商业版
|
||||
x-enum-varnames:
|
||||
- LicenseEditionFree
|
||||
- LicenseEditionContributor
|
||||
- LicenseEditionProfession
|
||||
- LicenseEditionEnterprise
|
||||
- LicenseEditionBusiness
|
||||
consts.ModelSettingMode:
|
||||
enum:
|
||||
- manual
|
||||
|
|
@ -1610,6 +1614,8 @@ definitions:
|
|||
url:
|
||||
type: string
|
||||
type: object
|
||||
domain.MessageContent:
|
||||
type: object
|
||||
domain.MessageFrom:
|
||||
enum:
|
||||
- 1
|
||||
|
|
@ -1871,6 +1877,8 @@ definitions:
|
|||
type: array
|
||||
stream:
|
||||
type: boolean
|
||||
stream_options:
|
||||
$ref: '#/definitions/domain.OpenAIStreamOptions'
|
||||
temperature:
|
||||
type: number
|
||||
tool_choice:
|
||||
|
|
@ -1952,7 +1960,7 @@ definitions:
|
|||
domain.OpenAIMessage:
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
$ref: '#/definitions/domain.MessageContent'
|
||||
name:
|
||||
type: string
|
||||
role:
|
||||
|
|
@ -1973,6 +1981,11 @@ definitions:
|
|||
required:
|
||||
- type
|
||||
type: object
|
||||
domain.OpenAIStreamOptions:
|
||||
properties:
|
||||
include_usage:
|
||||
type: boolean
|
||||
type: object
|
||||
domain.OpenAITool:
|
||||
properties:
|
||||
function:
|
||||
|
|
@ -2146,6 +2159,25 @@ definitions:
|
|||
role:
|
||||
$ref: '#/definitions/schema.RoleType'
|
||||
type: object
|
||||
domain.ShareNodeListItemResp:
|
||||
properties:
|
||||
emoji:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
parent_id:
|
||||
type: string
|
||||
permissions:
|
||||
$ref: '#/definitions/domain.NodePermissions'
|
||||
position:
|
||||
type: number
|
||||
type:
|
||||
$ref: '#/definitions/domain.NodeType'
|
||||
updated_at:
|
||||
type: string
|
||||
type: object
|
||||
domain.SimpleAuth:
|
||||
properties:
|
||||
enabled:
|
||||
|
|
@ -2488,12 +2520,24 @@ definitions:
|
|||
type: object
|
||||
domain.WidgetBotSettings:
|
||||
properties:
|
||||
btn_id:
|
||||
type: string
|
||||
btn_logo:
|
||||
type: string
|
||||
btn_position:
|
||||
type: string
|
||||
btn_style:
|
||||
type: string
|
||||
btn_text:
|
||||
type: string
|
||||
disclaimer:
|
||||
type: string
|
||||
is_open:
|
||||
type: boolean
|
||||
modal_position:
|
||||
type: string
|
||||
placeholder:
|
||||
type: string
|
||||
recommend_node_ids:
|
||||
items:
|
||||
type: string
|
||||
|
|
@ -2502,6 +2546,8 @@ definitions:
|
|||
items:
|
||||
type: string
|
||||
type: array
|
||||
search_mode:
|
||||
type: string
|
||||
theme_mode:
|
||||
type: string
|
||||
type: object
|
||||
|
|
@ -3075,6 +3121,10 @@ definitions:
|
|||
type: string
|
||||
kb_id:
|
||||
type: string
|
||||
list:
|
||||
items:
|
||||
$ref: '#/definitions/domain.ShareNodeListItemResp'
|
||||
type: array
|
||||
meta:
|
||||
$ref: '#/definitions/domain.NodeMeta'
|
||||
name:
|
||||
|
|
@ -5296,7 +5346,34 @@ paths:
|
|||
$ref: '#/definitions/domain.Response'
|
||||
summary: ChatWidget
|
||||
tags:
|
||||
- share_chat
|
||||
- Widget
|
||||
/share/v1/chat/widget/search:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: WidgetSearch
|
||||
parameters:
|
||||
- description: Comment
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/domain.ChatSearchReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/domain.Response'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/domain.ChatSearchResp'
|
||||
type: object
|
||||
summary: WidgetSearch
|
||||
tags:
|
||||
- Widget
|
||||
/share/v1/comment:
|
||||
post:
|
||||
consumes:
|
||||
|
|
|
|||
|
|
@ -405,6 +405,13 @@ type WidgetBotSettings struct {
|
|||
BtnLogo string `json:"btn_logo,omitempty"`
|
||||
RecommendQuestions []string `json:"recommend_questions,omitempty"`
|
||||
RecommendNodeIDs []string `json:"recommend_node_ids,omitempty"`
|
||||
BtnStyle string `json:"btn_style,omitempty"`
|
||||
BtnID string `json:"btn_id,omitempty"`
|
||||
BtnPosition string `json:"btn_position,omitempty"`
|
||||
ModalPosition string `json:"modal_position,omitempty"`
|
||||
SearchMode string `json:"search_mode,omitempty"`
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
Disclaimer string `json:"disclaimer,omitempty"`
|
||||
}
|
||||
|
||||
type BrandGroup struct {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const ContextKeyEditionLimitation contextKey = "edition_limitation"
|
||||
|
||||
type BaseEditionLimitation struct {
|
||||
MaxKb int `json:"max_kb"` // 知识库站点数量
|
||||
MaxNode int `json:"max_node"` // 单个知识库下文档数量
|
||||
MaxSSOUser int `json:"max_sso_users"` // SSO认证用户数量
|
||||
MaxAdmin int64 `json:"max_admin"` // 后台管理员数量
|
||||
AllowAdminPerm bool `json:"allow_admin_perm"` // 支持管理员分权控制
|
||||
AllowCustomCopyright bool `json:"allow_custom_copyright"` // 支持自定义版权信息
|
||||
AllowCommentAudit bool `json:"allow_comment_audit"` // 支持评论审核
|
||||
AllowAdvancedBot bool `json:"allow_advanced_bot"` // 支持高级机器人配置
|
||||
AllowWatermark bool `json:"allow_watermark"` // 支持水印
|
||||
AllowCopyProtection bool `json:"allow_copy_protection"` // 支持内容复制保护
|
||||
AllowOpenAIBotSettings bool `json:"allow_open_ai_bot_settings"` // 支持问答机器人
|
||||
}
|
||||
|
||||
var baseEditionLimitationDefault = BaseEditionLimitation{
|
||||
MaxKb: 1,
|
||||
MaxAdmin: 1,
|
||||
MaxNode: 300,
|
||||
}
|
||||
|
||||
func GetBaseEditionLimitation(c context.Context) BaseEditionLimitation {
|
||||
|
||||
edition, ok := c.Value(ContextKeyEditionLimitation).([]byte)
|
||||
if !ok {
|
||||
return baseEditionLimitationDefault
|
||||
}
|
||||
|
||||
var editionLimitation BaseEditionLimitation
|
||||
if err := json.Unmarshal(edition, &editionLimitation); err != nil {
|
||||
return baseEditionLimitationDefault
|
||||
}
|
||||
|
||||
return editionLimitation
|
||||
}
|
||||
|
|
@ -1,10 +1,17 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// OpenAI API 请求结构体
|
||||
type OpenAICompletionsRequest struct {
|
||||
Model string `json:"model" validate:"required"`
|
||||
Messages []OpenAIMessage `json:"messages" validate:"required"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
StreamOptions *OpenAIStreamOptions `json:"stream_options,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
MaxTokens *int `json:"max_tokens,omitempty"`
|
||||
TopP *float64 `json:"top_p,omitempty"`
|
||||
|
|
@ -17,9 +24,95 @@ type OpenAICompletionsRequest struct {
|
|||
ResponseFormat *OpenAIResponseFormat `json:"response_format,omitempty"`
|
||||
}
|
||||
|
||||
type OpenAIStreamOptions struct {
|
||||
IncludeUsage bool `json:"include_usage,omitempty"`
|
||||
}
|
||||
|
||||
// MessageContent 支持字符串或内容数组
|
||||
type MessageContent struct {
|
||||
isString bool
|
||||
strValue string
|
||||
arrValue []OpenAIContentPart
|
||||
}
|
||||
|
||||
// OpenAIContentPart 表示内容数组中的单个元素
|
||||
type OpenAIContentPart struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text,omitempty"`
|
||||
ImageURL *OpenAIContentPartURL `json:"image_url,omitempty"`
|
||||
}
|
||||
|
||||
// OpenAIContentPartURL represents the image_url field in content parts
|
||||
type OpenAIContentPartURL struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON 自定义解析,支持 string 或 array 格式
|
||||
func (mc *MessageContent) UnmarshalJSON(data []byte) error {
|
||||
// 尝试解析为字符串
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err == nil {
|
||||
mc.isString = true
|
||||
mc.strValue = str
|
||||
return nil
|
||||
}
|
||||
|
||||
// 尝试解析为数组
|
||||
var arr []OpenAIContentPart
|
||||
if err := json.Unmarshal(data, &arr); err == nil {
|
||||
mc.isString = false
|
||||
mc.arrValue = arr
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("content must be string or array")
|
||||
}
|
||||
|
||||
// MarshalJSON 自定义序列化
|
||||
func (mc *MessageContent) MarshalJSON() ([]byte, error) {
|
||||
if mc.isString {
|
||||
return json.Marshal(mc.strValue)
|
||||
}
|
||||
return json.Marshal(mc.arrValue)
|
||||
}
|
||||
|
||||
// NewStringContent 创建字符串类型的 MessageContent
|
||||
func NewStringContent(s string) *MessageContent {
|
||||
return &MessageContent{
|
||||
isString: true,
|
||||
strValue: s,
|
||||
}
|
||||
}
|
||||
|
||||
// NewArrayContent 创建数组类型的 MessageContent
|
||||
func NewArrayContent(parts []OpenAIContentPart) *MessageContent {
|
||||
return &MessageContent{
|
||||
isString: false,
|
||||
arrValue: parts,
|
||||
}
|
||||
}
|
||||
|
||||
// String 获取文本内容
|
||||
func (mc *MessageContent) String() string {
|
||||
if mc.isString {
|
||||
return mc.strValue
|
||||
}
|
||||
// 从数组中提取文本
|
||||
var builder strings.Builder
|
||||
for _, part := range mc.arrValue {
|
||||
if part.Type == "text" {
|
||||
if builder.Len() > 0 && part.Text != "" {
|
||||
builder.WriteString(" ")
|
||||
}
|
||||
builder.WriteString(part.Text)
|
||||
}
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
type OpenAIMessage struct {
|
||||
Role string `json:"role" validate:"required"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Content *MessageContent `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ToolCalls []OpenAIToolCall `json:"tool_calls,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
|
|
@ -90,6 +183,7 @@ type OpenAIStreamResponse struct {
|
|||
Created int64 `json:"created"`
|
||||
Model string `json:"model"`
|
||||
Choices []OpenAIStreamChoice `json:"choices"`
|
||||
Usage *OpenAIUsage `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
type OpenAIStreamChoice struct {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMessageContent_UnmarshalJSON_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
expected string
|
||||
}{
|
||||
{"simple string", `"hello"`, "hello"},
|
||||
{"with quotes", `"say \"hello\""`, `say "hello"`},
|
||||
{"with newline", `"line1\nline2"`, "line1\nline2"},
|
||||
{"empty string", `""`, ""},
|
||||
{"unicode", `"你好 🌍"`, "你好 🌍"},
|
||||
{"special chars", `"Hello \"World\"\nNew Line\tTab"`, "Hello \"World\"\nNew Line\tTab"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var mc MessageContent
|
||||
err := json.Unmarshal([]byte(tt.json), &mc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, mc.String())
|
||||
assert.True(t, mc.isString)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageContent_UnmarshalJSON_Array(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"single text part",
|
||||
`[{"type":"text","text":"Hello"}]`,
|
||||
"Hello",
|
||||
},
|
||||
{
|
||||
"multiple text parts",
|
||||
`[{"type":"text","text":"Hello"},{"type":"text","text":"World"}]`,
|
||||
"Hello World",
|
||||
},
|
||||
{
|
||||
"mixed types with image",
|
||||
`[{"type":"text","text":"Look at this"},{"type":"image_url","image_url":{"url":"https://example.com/img.png"}},{"type":"text","text":"image"}]`,
|
||||
"Look at this image",
|
||||
},
|
||||
{
|
||||
"empty array",
|
||||
`[]`,
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var mc MessageContent
|
||||
err := json.Unmarshal([]byte(tt.json), &mc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, mc.String())
|
||||
assert.False(t, mc.isString)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageContent_UnmarshalJSON_Invalid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
json string
|
||||
}{
|
||||
{"number", `123`},
|
||||
{"boolean", `true`},
|
||||
{"object", `{"key":"value"}`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var mc MessageContent
|
||||
err := json.Unmarshal([]byte(tt.json), &mc)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "content must be string or array")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageContent_UnmarshalJSON_Null(t *testing.T) {
|
||||
var mc *MessageContent
|
||||
err := json.Unmarshal([]byte(`null`), &mc)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, mc)
|
||||
}
|
||||
|
||||
func TestMessageContent_MarshalJSON_String(t *testing.T) {
|
||||
mc := NewStringContent("Hello World")
|
||||
data, err := json.Marshal(mc)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `"Hello World"`, string(data))
|
||||
}
|
||||
|
||||
func TestMessageContent_MarshalJSON_Array(t *testing.T) {
|
||||
mc := NewArrayContent([]OpenAIContentPart{
|
||||
{Type: "text", Text: "Hello"},
|
||||
{Type: "text", Text: "World"},
|
||||
})
|
||||
data, err := json.Marshal(mc)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `[{"type":"text","text":"Hello"},{"type":"text","text":"World"}]`, string(data))
|
||||
}
|
||||
|
||||
func TestMessageContent_Roundtrip_String(t *testing.T) {
|
||||
original := NewStringContent("Test message with \"quotes\" and \nnewlines")
|
||||
|
||||
// Marshal
|
||||
data, err := json.Marshal(original)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Unmarshal
|
||||
var decoded MessageContent
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original.String(), decoded.String())
|
||||
assert.Equal(t, original.isString, decoded.isString)
|
||||
}
|
||||
|
||||
func TestMessageContent_Roundtrip_Array(t *testing.T) {
|
||||
parts := []OpenAIContentPart{
|
||||
{Type: "text", Text: "Part 1"},
|
||||
{Type: "text", Text: "Part 2"},
|
||||
}
|
||||
original := NewArrayContent(parts)
|
||||
|
||||
// Marshal
|
||||
data, err := json.Marshal(original)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Unmarshal
|
||||
var decoded MessageContent
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify
|
||||
assert.Equal(t, original.String(), decoded.String())
|
||||
assert.Equal(t, original.isString, decoded.isString)
|
||||
}
|
||||
|
||||
func TestNewStringContent(t *testing.T) {
|
||||
mc := NewStringContent("test")
|
||||
assert.NotNil(t, mc)
|
||||
assert.True(t, mc.isString)
|
||||
assert.Equal(t, "test", mc.strValue)
|
||||
assert.Equal(t, "test", mc.String())
|
||||
}
|
||||
|
||||
func TestNewArrayContent(t *testing.T) {
|
||||
parts := []OpenAIContentPart{
|
||||
{Type: "text", Text: "Hello"},
|
||||
}
|
||||
mc := NewArrayContent(parts)
|
||||
assert.NotNil(t, mc)
|
||||
assert.False(t, mc.isString)
|
||||
assert.Equal(t, parts, mc.arrValue)
|
||||
assert.Equal(t, "Hello", mc.String())
|
||||
}
|
||||
|
||||
func TestMessageContent_String_EmptyArray(t *testing.T) {
|
||||
mc := NewArrayContent([]OpenAIContentPart{})
|
||||
assert.Equal(t, "", mc.String())
|
||||
}
|
||||
|
||||
func TestMessageContent_String_NoTextParts(t *testing.T) {
|
||||
mc := NewArrayContent([]OpenAIContentPart{
|
||||
{Type: "image_url", Text: ""},
|
||||
})
|
||||
assert.Equal(t, "", mc.String())
|
||||
}
|
||||
|
|
@ -49,6 +49,7 @@ require (
|
|||
github.com/sbzhu/weworkapi_golang v0.0.0-20210525081115-1799804a7c8d
|
||||
github.com/silenceper/wechat/v2 v2.1.9
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/swaggo/echo-swagger v1.4.1
|
||||
github.com/swaggo/swag v1.16.5
|
||||
github.com/tidwall/gjson v1.14.1
|
||||
|
|
@ -98,6 +99,7 @@ require (
|
|||
github.com/cloudwego/eino-ext/components/model/openai v0.0.0-20250710065240-482d48888f25 // indirect
|
||||
github.com/cloudwego/eino-ext/libs/acl/openai v0.0.0-20250626133421-3c142631c961 // indirect
|
||||
github.com/cohesion-org/deepseek-go v1.2.8 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
|
|
@ -165,6 +167,7 @@ require (
|
|||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ func NewShareChatHandler(
|
|||
share.POST("/search", h.ChatSearch, h.ShareAuthMiddleware.Authorize)
|
||||
share.POST("/completions", h.ChatCompletions)
|
||||
share.POST("/widget", h.ChatWidget)
|
||||
share.POST("/widget/search", h.WidgetSearch)
|
||||
share.POST("/feedback", h.FeedBack)
|
||||
return h
|
||||
}
|
||||
|
|
@ -131,7 +132,7 @@ func (h *ShareChatHandler) ChatMessage(c echo.Context) error {
|
|||
//
|
||||
// @Summary ChatWidget
|
||||
// @Description ChatWidget
|
||||
// @Tags share_chat
|
||||
// @Tags Widget
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param app_type query string true "app type"
|
||||
|
|
@ -268,7 +269,9 @@ func (h *ShareChatHandler) ChatCompletions(c echo.Context) error {
|
|||
var lastUserMessage string
|
||||
for i := len(req.Messages) - 1; i >= 0; i-- {
|
||||
if req.Messages[i].Role == "user" {
|
||||
lastUserMessage = req.Messages[i].Content
|
||||
if req.Messages[i].Content != nil {
|
||||
lastUserMessage = req.Messages[i].Content.String()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -345,11 +348,12 @@ func (h *ShareChatHandler) handleOpenAIStreamResponse(c echo.Context, eventCh <-
|
|||
Index: 0,
|
||||
Delta: domain.OpenAIMessage{
|
||||
Role: "assistant",
|
||||
Content: event.Content,
|
||||
Content: domain.NewStringContent(event.Content),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := h.writeOpenAIStreamEvent(c, streamResp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -397,7 +401,7 @@ func (h *ShareChatHandler) handleOpenAINonStreamResponse(c echo.Context, eventCh
|
|||
Index: 0,
|
||||
Message: domain.OpenAIMessage{
|
||||
Role: "assistant",
|
||||
Content: content,
|
||||
Content: domain.NewStringContent(content),
|
||||
},
|
||||
FinishReason: "stop",
|
||||
},
|
||||
|
|
@ -441,7 +445,7 @@ func stringPtr(s string) *string {
|
|||
return &s
|
||||
}
|
||||
|
||||
// ChatMessage chat search
|
||||
// ChatSearch searches chat messages in shared knowledge base
|
||||
//
|
||||
// @Summary ChatSearch
|
||||
// @Description ChatSearch
|
||||
|
|
@ -484,3 +488,43 @@ func (h *ShareChatHandler) ChatSearch(c echo.Context) error {
|
|||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
||||
// WidgetSearch
|
||||
//
|
||||
// @Summary WidgetSearch
|
||||
// @Description WidgetSearch
|
||||
// @Tags Widget
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body domain.ChatSearchReq true "Comment"
|
||||
// @Success 200 {object} domain.Response{data=domain.ChatSearchResp}
|
||||
// @Router /share/v1/chat/widget/search [post]
|
||||
func (h *ShareChatHandler) WidgetSearch(c echo.Context) error {
|
||||
var req domain.ChatSearchReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "parse request failed", err)
|
||||
}
|
||||
req.KBID = c.Request().Header.Get("X-KB-ID")
|
||||
if err := c.Validate(&req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// validate widget info
|
||||
widgetAppInfo, err := h.appUsecase.GetWidgetAppInfo(c.Request().Context(), req.KBID)
|
||||
if err != nil {
|
||||
h.logger.Error("get widget app info failed", log.Error(err))
|
||||
return h.sendErrMsg(c, "get app info error")
|
||||
}
|
||||
if !widgetAppInfo.Settings.WidgetBotSettings.IsOpen {
|
||||
return h.sendErrMsg(c, "widget is not open")
|
||||
}
|
||||
|
||||
req.RemoteIP = c.RealIP()
|
||||
|
||||
resp, err := h.chatUsecase.Search(ctx, &req)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to search docs", err)
|
||||
}
|
||||
return h.NewResponseWithData(c, resp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/handler"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
|
|
@ -157,7 +156,7 @@ func (h *ShareCommentHandler) GetCommentList(c echo.Context) error {
|
|||
}
|
||||
|
||||
// 查询数据库获取所有评论-->0 所有, 1,2 为需要审核的评论
|
||||
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID, consts.GetLicenseEdition(c))
|
||||
commentsList, err := h.usecase.GetCommentListByNodeID(ctx, nodeID)
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get comment list", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,5 +91,15 @@ func (h *ShareNodeHandler) GetNodeDetail(c echo.Context) error {
|
|||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get node detail", err)
|
||||
}
|
||||
|
||||
// If the node is a folder, return the list of child nodes
|
||||
if node.Type == domain.NodeTypeFolder {
|
||||
childNodes, err := h.usecase.GetNodeReleaseListByParentID(c.Request().Context(), kbID, id, domain.GetAuthID(c))
|
||||
if err != nil {
|
||||
return h.NewResponseWithError(c, "failed to get child nodes", err)
|
||||
}
|
||||
node.List = childNodes
|
||||
}
|
||||
|
||||
return h.NewResponseWithData(c, node)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
v1 "github.com/chaitin/panda-wiki/api/kb/v1"
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
)
|
||||
|
||||
// KBUserList
|
||||
|
|
@ -55,8 +56,8 @@ func (h *KnowledgeBaseHandler) KBUserInvite(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
|
||||
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
|
||||
}
|
||||
|
||||
err := h.usecase.KBUserInvite(c.Request().Context(), req)
|
||||
|
|
@ -87,8 +88,8 @@ func (h *KnowledgeBaseHandler) KBUserUpdate(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "validate request failed", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) != consts.LicenseEditionEnterprise && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "非企业版本只能使用完全控制权限", nil)
|
||||
if !domain.GetBaseEditionLimitation(c.Request().Context()).AllowAdminPerm && req.Perm != consts.UserKBPermissionFullControl {
|
||||
return h.NewResponseWithError(c, "当前版本不支持管理员分权控制", nil)
|
||||
}
|
||||
|
||||
err := h.usecase.UpdateUserKB(c.Request().Context(), req)
|
||||
|
|
|
|||
|
|
@ -91,11 +91,7 @@ func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "ports is required", nil)
|
||||
}
|
||||
|
||||
req.MaxKB = 1
|
||||
maxKB := c.Get("max_kb")
|
||||
if maxKB != nil {
|
||||
req.MaxKB = maxKB.(int)
|
||||
}
|
||||
req.MaxKB = domain.GetBaseEditionLimitation(c.Request().Context()).MaxKb
|
||||
|
||||
did, err := h.usecase.CreateKnowledgeBase(c.Request().Context(), &req)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func NewModelHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *
|
|||
return handler
|
||||
}
|
||||
|
||||
// get model list
|
||||
// GetModelList
|
||||
//
|
||||
// @Summary get model list
|
||||
// @Description get model list
|
||||
|
|
@ -66,7 +66,7 @@ func (h *ModelHandler) GetModelList(c echo.Context) error {
|
|||
return h.NewResponseWithData(c, models)
|
||||
}
|
||||
|
||||
// create model
|
||||
// CreateModel
|
||||
//
|
||||
// @Summary create model
|
||||
// @Description create model
|
||||
|
|
@ -85,9 +85,6 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
|
||||
return h.NewResponseWithError(c, "联创版只能使用百智云模型哦~", nil)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
|
||||
param := domain.ModelParam{}
|
||||
|
|
@ -112,7 +109,7 @@ func (h *ModelHandler) CreateModel(c echo.Context) error {
|
|||
return h.NewResponseWithData(c, model)
|
||||
}
|
||||
|
||||
// update model
|
||||
// UpdateModel
|
||||
//
|
||||
// @Description update model
|
||||
// @Tags model
|
||||
|
|
@ -130,9 +127,6 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
|
|||
return h.NewResponseWithError(c, "invalid request", err)
|
||||
}
|
||||
|
||||
if consts.GetLicenseEdition(c) == consts.LicenseEditionContributor && req.Provider != domain.ModelProviderBrandBaiZhiCloud {
|
||||
return h.NewResponseWithError(c, "联创版只能使用百智云模型哦~", nil)
|
||||
}
|
||||
ctx := c.Request().Context()
|
||||
if err := h.usecase.Update(ctx, &req); err != nil {
|
||||
return h.NewResponseWithError(c, "update model failed", err)
|
||||
|
|
@ -140,7 +134,7 @@ func (h *ModelHandler) UpdateModel(c echo.Context) error {
|
|||
return h.NewResponseWithData(c, nil)
|
||||
}
|
||||
|
||||
// check model
|
||||
// CheckModel
|
||||
//
|
||||
// @Summary check model
|
||||
// @Description check model
|
||||
|
|
|
|||
|
|
@ -81,15 +81,13 @@ func (h *NodeHandler) CreateNode(c echo.Context) error {
|
|||
if err := c.Validate(req); err != nil {
|
||||
return h.NewResponseWithError(c, "validate request body failed", err)
|
||||
}
|
||||
req.MaxNode = 300
|
||||
if maxNode := c.Get("max_node"); maxNode != nil {
|
||||
req.MaxNode = maxNode.(int)
|
||||
}
|
||||
|
||||
req.MaxNode = domain.GetBaseEditionLimitation(ctx).MaxNode
|
||||
|
||||
id, err := h.usecase.Create(c.Request().Context(), req, authInfo.UserId)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrMaxNodeLimitReached) {
|
||||
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到联创版或企业版", nil)
|
||||
return h.NewResponseWithError(c, "已达到最大文档数量限制,请升级到更高版本", nil)
|
||||
}
|
||||
return h.NewResponseWithError(c, "create node failed", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ type AuthMiddleware interface {
|
|||
Authorize(next echo.HandlerFunc) echo.HandlerFunc
|
||||
ValidateUserRole(role consts.UserRole) echo.MiddlewareFunc
|
||||
ValidateKBUserPerm(role consts.UserKBPermission) echo.MiddlewareFunc
|
||||
ValidateLicenseEdition(edition consts.LicenseEdition) echo.MiddlewareFunc
|
||||
ValidateLicenseEdition(edition ...consts.LicenseEdition) echo.MiddlewareFunc
|
||||
MustGetUserID(c echo.Context) (string, bool)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
|
@ -194,7 +195,7 @@ func (m *JWTMiddleware) ValidateKBUserPerm(perm consts.UserKBPermission) echo.Mi
|
|||
}
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) ValidateLicenseEdition(needEdition consts.LicenseEdition) echo.MiddlewareFunc {
|
||||
func (m *JWTMiddleware) ValidateLicenseEdition(needEditions ...consts.LicenseEdition) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
|
||||
|
|
@ -206,7 +207,7 @@ func (m *JWTMiddleware) ValidateLicenseEdition(needEdition consts.LicenseEdition
|
|||
})
|
||||
}
|
||||
|
||||
if edition < needEdition {
|
||||
if !slices.Contains(needEditions, edition) {
|
||||
return c.JSON(http.StatusForbidden, domain.PWResponse{
|
||||
Success: false,
|
||||
Message: "Unauthorized ValidateLicenseEdition",
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit c4dc498df094cb617d31c95580db8239a445d652
|
||||
Subproject commit bb1b17dd5c7d72d40f6a1198b1604f4d3c44116e
|
||||
|
|
@ -300,8 +300,8 @@ func (r *AuthRepo) GetOrCreateAuth(ctx context.Context, auth *domain.Auth, sourc
|
|||
return err
|
||||
}
|
||||
|
||||
if int(count) >= licenseEdition.GetMaxAuth(sourceType) {
|
||||
return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, licenseEdition.GetMaxAuth(sourceType))
|
||||
if int(count) >= domain.GetBaseEditionLimitation(ctx).MaxSSOUser {
|
||||
return fmt.Errorf("exceed max auth limit for kb %s, current count: %d, max limit: %d", auth.KBID, count, domain.GetBaseEditionLimitation(ctx).MaxSSOUser)
|
||||
}
|
||||
|
||||
auth.LastLoginTime = time.Now()
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ func (r *CommentRepository) CreateComment(ctx context.Context, comment *domain.C
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string, edition consts.LicenseEdition) ([]*domain.ShareCommentListItem, int64, error) {
|
||||
func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string) ([]*domain.ShareCommentListItem, int64, error) {
|
||||
// 按照时间排序来查询node_id的comments
|
||||
var comments []*domain.ShareCommentListItem
|
||||
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("node_id = ?", nodeID)
|
||||
|
||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
|
||||
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit {
|
||||
query = query.Where("status = ?", domain.CommentStatusAccepted) //accepted
|
||||
}
|
||||
|
||||
|
|
@ -50,14 +50,14 @@ func (r *CommentRepository) GetCommentList(ctx context.Context, nodeID string, e
|
|||
|
||||
func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domain.CommentListReq, edition consts.LicenseEdition) ([]*domain.CommentListItem, int64, error) {
|
||||
comments := []*domain.CommentListItem{}
|
||||
query := r.db.Model(&domain.Comment{}).Where("comments.kb_id = ?", req.KbID)
|
||||
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("comments.kb_id = ?", req.KbID)
|
||||
var count int64
|
||||
if req.Status == nil {
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
} else {
|
||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionEnterprise {
|
||||
if domain.GetBaseEditionLimitation(ctx).AllowCommentAudit {
|
||||
query = query.Where("comments.status = ?", *req.Status)
|
||||
}
|
||||
// 按照时间排序来查询kb_id的comments ->reject pending accepted
|
||||
|
|
@ -84,7 +84,7 @@ func (r *CommentRepository) GetCommentListByKbID(ctx context.Context, req *domai
|
|||
|
||||
func (r *CommentRepository) DeleteCommentList(ctx context.Context, commentID []string) error {
|
||||
// 批量删除指定id的comment,获取删除的总的数量、
|
||||
query := r.db.Model(&domain.Comment{}).Where("id IN (?)", commentID)
|
||||
query := r.db.WithContext(ctx).Model(&domain.Comment{}).Where("id IN (?)", commentID)
|
||||
|
||||
if err := query.Delete(&domain.Comment{}).Error; err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -341,11 +341,12 @@ func (r *KnowledgeBaseRepository) CreateKnowledgeBase(ctx context.Context, maxKB
|
|||
Name: kb.Name,
|
||||
Type: domain.AppTypeWeb,
|
||||
Settings: domain.AppSettings{
|
||||
Title: kb.Name,
|
||||
Desc: kb.Name,
|
||||
Keyword: kb.Name,
|
||||
Icon: domain.DefaultPandaWikiIconB64,
|
||||
WelcomeStr: fmt.Sprintf("欢迎使用%s", kb.Name),
|
||||
Title: kb.Name,
|
||||
Desc: kb.Name,
|
||||
Keyword: kb.Name,
|
||||
AutoSitemap: true,
|
||||
Icon: domain.DefaultPandaWikiIconB64,
|
||||
WelcomeStr: fmt.Sprintf("欢迎使用%s", kb.Name),
|
||||
Btns: []any{
|
||||
AppBtn{
|
||||
ID: uuid.New().String(),
|
||||
|
|
|
|||
|
|
@ -683,7 +683,7 @@ func (r *NodeRepository) GetNodeReleaseListByKBID(ctx context.Context, kbID stri
|
|||
Where("kb_release_node_releases.kb_id = ?", kbID).
|
||||
Where("kb_release_node_releases.release_id = ?", kbRelease.ID).
|
||||
Where("nodes.permissions->>'visible' != ?", consts.NodeAccessPermClosed).
|
||||
Select("node_releases.node_id as id, node_releases.name, node_releases.type, node_releases.parent_id, node_releases.position, node_releases.meta->>'emoji' as emoji, node_releases.updated_at, nodes.permissions").
|
||||
Select("node_releases.node_id as id, node_releases.name, node_releases.type, node_releases.parent_id, nodes.position, node_releases.meta->>'emoji' as emoji, node_releases.updated_at, nodes.permissions").
|
||||
Find(&nodes).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,18 +60,14 @@ func (r *UserRepository) CreateUser(ctx context.Context, user *domain.User, edit
|
|||
}
|
||||
user.Password = string(hashedPassword)
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if edition == consts.LicenseEditionContributor || edition == consts.LicenseEditionFree {
|
||||
var count int64
|
||||
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if edition == consts.LicenseEditionFree && count >= 1 {
|
||||
return errors.New("free edition only allows 1 user")
|
||||
}
|
||||
if edition == consts.LicenseEditionContributor && count >= 5 {
|
||||
return errors.New("contributor edition only allows 5 user")
|
||||
}
|
||||
var count int64
|
||||
if err := tx.Model(&domain.User{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count >= domain.GetBaseEditionLimitation(ctx).MaxAdmin {
|
||||
return fmt.Errorf("exceed max admin limit, current count: %d, max limit: %d", count, domain.GetBaseEditionLimitation(ctx).MaxAdmin)
|
||||
}
|
||||
|
||||
if err := tx.Create(user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,34 +88,38 @@ func NewAppUsecase(
|
|||
}
|
||||
|
||||
func (u *AppUsecase) ValidateUpdateApp(ctx context.Context, id string, req *domain.UpdateAppReq, edition consts.LicenseEdition) error {
|
||||
switch edition {
|
||||
case consts.LicenseEditionFree:
|
||||
app, err := u.repo.GetAppDetail(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if app.Settings.WatermarkContent != req.Settings.WatermarkContent ||
|
||||
app.Settings.WatermarkSetting != req.Settings.WatermarkSetting ||
|
||||
app.Settings.ContributeSettings != req.Settings.ContributeSettings ||
|
||||
app.Settings.CopySetting != req.Settings.CopySetting {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
case consts.LicenseEditionContributor:
|
||||
app, err := u.repo.GetAppDetail(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if app.Settings.WatermarkContent != req.Settings.WatermarkContent ||
|
||||
app.Settings.WatermarkSetting != req.Settings.WatermarkSetting ||
|
||||
app.Settings.CopySetting != req.Settings.CopySetting {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
case consts.LicenseEditionEnterprise:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported license type: %d", edition)
|
||||
app, err := u.repo.GetAppDetail(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
limitation := domain.GetBaseEditionLimitation(ctx)
|
||||
if !limitation.AllowCopyProtection && app.Settings.CopySetting != req.Settings.CopySetting {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if !limitation.AllowWatermark {
|
||||
if app.Settings.WatermarkSetting != req.Settings.WatermarkSetting || app.Settings.WatermarkContent != req.Settings.WatermarkContent {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
if !limitation.AllowAdvancedBot {
|
||||
if !slices.Equal(app.Settings.WechatServiceContainKeywords, req.Settings.WechatServiceContainKeywords) ||
|
||||
!slices.Equal(app.Settings.WechatServiceEqualKeywords, req.Settings.WechatServiceEqualKeywords) {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
|
||||
if !limitation.AllowCommentAudit && app.Settings.WebAppCommentSettings.ModerationEnable != req.Settings.WebAppCommentSettings.ModerationEnable {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if !limitation.AllowOpenAIBotSettings {
|
||||
if app.Settings.OpenAIAPIBotSettings.IsEnabled != req.Settings.OpenAIAPIBotSettings.IsEnabled || app.Settings.OpenAIAPIBotSettings.SecretKey != req.Settings.OpenAIAPIBotSettings.SecretKey {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -618,8 +622,8 @@ func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId
|
|||
}
|
||||
showBrand := true
|
||||
defaultDisclaimer := "本回答由 PandaWiki 基于 AI 生成,仅供参考。"
|
||||
licenseEdition, _ := ctx.Value(consts.ContextKeyEdition).(consts.LicenseEdition)
|
||||
if licenseEdition < consts.LicenseEditionEnterprise {
|
||||
|
||||
if !domain.GetBaseEditionLimitation(ctx).AllowCustomCopyright {
|
||||
appInfo.Settings.WebAppCustomSettings.ShowBrandInfo = &showBrand
|
||||
appInfo.Settings.DisclaimerSettings.Content = &defaultDisclaimer
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ func (u *CommentUsecase) CreateComment(ctx context.Context, commentReq *domain.C
|
|||
return CommentStr, nil
|
||||
}
|
||||
|
||||
func (u *CommentUsecase) GetCommentListByNodeID(ctx context.Context, nodeID string, edition consts.LicenseEdition) (*domain.PaginatedResult[[]*domain.ShareCommentListItem], error) {
|
||||
comments, total, err := u.CommentRepo.GetCommentList(ctx, nodeID, edition)
|
||||
func (u *CommentUsecase) GetCommentListByNodeID(ctx context.Context, nodeID string) (*domain.PaginatedResult[[]*domain.ShareCommentListItem], error) {
|
||||
comments, total, err := u.CommentRepo.GetCommentList(ctx, nodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -350,6 +350,56 @@ func (u *NodeUsecase) GetNodeReleaseListByKBID(ctx context.Context, kbID string,
|
|||
return items, nil
|
||||
}
|
||||
|
||||
func (u *NodeUsecase) GetNodeReleaseListByParentID(ctx context.Context, kbID, parentID string, authId uint) ([]*domain.ShareNodeListItemResp, error) {
|
||||
// 一次性查询所有节点
|
||||
allNodes, err := u.nodeRepo.GetNodeReleaseListByKBID(ctx, kbID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodeGroupIds, err := u.GetNodeIdsByAuthId(ctx, authId, consts.NodePermNameVisible)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 先过滤权限
|
||||
visibleNodes := make([]*domain.ShareNodeListItemResp, 0)
|
||||
for i, node := range allNodes {
|
||||
switch node.Permissions.Visible {
|
||||
case consts.NodeAccessPermOpen:
|
||||
visibleNodes = append(visibleNodes, allNodes[i])
|
||||
case consts.NodeAccessPermPartial:
|
||||
if slices.Contains(nodeGroupIds, node.ID) {
|
||||
visibleNodes = append(visibleNodes, allNodes[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建父子关系映射
|
||||
childrenMap := make(map[string][]*domain.ShareNodeListItemResp)
|
||||
for _, node := range visibleNodes {
|
||||
childrenMap[node.ParentID] = append(childrenMap[node.ParentID], node)
|
||||
}
|
||||
|
||||
// 递归收集所有后代节点
|
||||
result := make([]*domain.ShareNodeListItemResp, 0)
|
||||
u.collectDescendants(parentID, childrenMap, &result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// collectDescendants 递归收集所有后代节点
|
||||
func (u *NodeUsecase) collectDescendants(parentID string, childrenMap map[string][]*domain.ShareNodeListItemResp, result *[]*domain.ShareNodeListItemResp) {
|
||||
children := childrenMap[parentID]
|
||||
for _, child := range children {
|
||||
*result = append(*result, child)
|
||||
// 如果是文件夹,递归收集其子节点
|
||||
if child.Type == domain.NodeTypeFolder {
|
||||
u.collectDescendants(child.ID, childrenMap, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *NodeUsecase) GetNodeIdsByAuthId(ctx context.Context, authId uint, PermName consts.NodePermName) ([]string, error) {
|
||||
authGroups, err := u.authRepo.GetAuthGroupWithParentsByAuthId(ctx, authId)
|
||||
if err != nil {
|
||||
|
|
@ -407,7 +457,7 @@ func (u *NodeUsecase) GetNodePermissionsByID(ctx context.Context, id, kbID strin
|
|||
}
|
||||
|
||||
func (u *NodeUsecase) ValidateNodePermissionsEdit(req v1.NodePermissionEditReq, edition consts.LicenseEdition) error {
|
||||
if edition != consts.LicenseEditionEnterprise {
|
||||
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) {
|
||||
if req.Permissions.Answerable == consts.NodeAccessPermPartial || req.Permissions.Visitable == consts.NodeAccessPermPartial || req.Permissions.Visible == consts.NodeAccessPermPartial {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
|
|
@ -67,12 +68,12 @@ func (u *StatUseCase) ValidateStatDay(statDay consts.StatDay, edition consts.Lic
|
|||
case consts.StatDay1:
|
||||
return nil
|
||||
case consts.StatDay7:
|
||||
if edition < consts.LicenseEditionContributor {
|
||||
if edition == consts.LicenseEditionFree {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
case consts.StatDay30, consts.StatDay90:
|
||||
if edition < consts.LicenseEditionEnterprise {
|
||||
if !slices.Contains([]consts.LicenseEdition{consts.LicenseEditionBusiness, consts.LicenseEditionEnterprise}, edition) {
|
||||
return domain.ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -588,6 +588,7 @@ export type ChatConversationItem = {
|
|||
export type ChatConversationPair = {
|
||||
user: string;
|
||||
assistant: string;
|
||||
thinking_content: string;
|
||||
created_at: string;
|
||||
info: {
|
||||
feedback_content: string;
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
File diff suppressed because one or more lines are too long
|
|
@ -144,7 +144,7 @@ const Config: React.FC<ConfigProps> = ({ setIsEdit, id }) => {
|
|||
callback();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [subscribe]);
|
||||
}, [subscribe, appPreviewData, id]);
|
||||
|
||||
return (
|
||||
<StyledCommonWrapper>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { setAppPreviewData } from '@/store/slices/config';
|
|||
import { DomainSocialMediaAccount } from '@/request/types';
|
||||
import Switch from '../basicComponents/Switch';
|
||||
import DragSocialInfo from '../basicComponents/DragSocialInfo';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface FooterConfigProps {
|
||||
data?: AppDetail | null;
|
||||
|
|
@ -75,9 +77,6 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
);
|
||||
const footer_show_intro = watch('footer_show_intro');
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
useEffect(() => {
|
||||
if (isEdit && appPreviewData) {
|
||||
setValue(
|
||||
|
|
@ -506,29 +505,33 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
)}
|
||||
/>
|
||||
</Stack>
|
||||
{isEnterprise && (
|
||||
<Stack direction={'column'} gap={2}>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
lineHeight: '22px',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 600,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: 4,
|
||||
height: 12,
|
||||
bgcolor: '#3248F2',
|
||||
borderRadius: '2px',
|
||||
mr: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
PandaWiki 版权信息
|
||||
</Box>
|
||||
|
||||
<Stack direction={'column'} gap={2}>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
lineHeight: '22px',
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: 600,
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: 4,
|
||||
height: 12,
|
||||
bgcolor: '#3248F2',
|
||||
borderRadius: '2px',
|
||||
mr: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
PandaWiki 版权信息
|
||||
</Box>
|
||||
<VersionMask
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
sx={{ inset: '-8px 0' }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='show_brand_info'
|
||||
|
|
@ -548,7 +551,6 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
<Switch
|
||||
sx={{ marginLeft: 'auto' }}
|
||||
{...field}
|
||||
disabled={!isEnterprise}
|
||||
checked={field?.value === false ? false : true}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.checked);
|
||||
|
|
@ -558,8 +560,8 @@ const FooterConfig = ({ data, setIsEdit, isEdit }: FooterConfigProps) => {
|
|||
</Stack>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</VersionMask>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { KnowledgeBaseListItem } from '@/api';
|
||||
import { useURLSearchParams } from '@/hooks';
|
||||
import { useFeatureValue } from '@/hooks';
|
||||
import { ConstsUserRole } from '@/request/types';
|
||||
import { useAppDispatch, useAppSelector } from '@/store';
|
||||
import { setKbC, setKbId } from '@/store/slices/config';
|
||||
|
|
@ -23,14 +24,14 @@ const KBSelect = () => {
|
|||
|
||||
const dispatch = useAppDispatch();
|
||||
const [_, setSearchParams] = useURLSearchParams();
|
||||
const { kb_id, kbList, license, user } = useAppSelector(
|
||||
state => state.config,
|
||||
);
|
||||
const { kb_id, kbList, user } = useAppSelector(state => state.config);
|
||||
|
||||
const [modifyOpen, setModifyOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [opraData, setOpraData] = useState<KnowledgeBaseListItem | null>(null);
|
||||
|
||||
const wikiCount = useFeatureValue('wikiCount');
|
||||
|
||||
return (
|
||||
<>
|
||||
{(kbList || []).length > 0 && (
|
||||
|
|
@ -121,8 +122,7 @@ const KBSelect = () => {
|
|||
}}
|
||||
fullWidth
|
||||
disabled={
|
||||
(license.edition === 0 && (kbList || []).length >= 1) ||
|
||||
(license.edition === 1 && (kbList || []).length >= 3) ||
|
||||
(kbList || []).length >= wikiCount ||
|
||||
user.role === ConstsUserRole.UserRoleUser
|
||||
}
|
||||
onClick={event => {
|
||||
|
|
|
|||
|
|
@ -3,27 +3,20 @@ import {
|
|||
getApiV1License,
|
||||
deleteApiV1License,
|
||||
} from '@/request/pro/License';
|
||||
import { PostApiV1LicensePayload } from '@/request/pro/types';
|
||||
import HelpCenter from '@/assets/json/help-center.json';
|
||||
import Takeoff from '@/assets/json/takeoff.json';
|
||||
import error from '@/assets/json/error.json';
|
||||
import IconUpgrade from '@/assets/json/upgrade.json';
|
||||
import Upload from '@/components/UploadFile/Drag';
|
||||
import { EditionType } from '@/constant/enums';
|
||||
import { useVersionInfo } from '@/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '@/store';
|
||||
import { setLicense } from '@/store/slices/config';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Stack,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import { Box, Button, IconButton, Stack, TextField } from '@mui/material';
|
||||
import { CusTabs, Icon, message, Modal } from '@ctzhian/ui';
|
||||
import dayjs from 'dayjs';
|
||||
import { useState } from 'react';
|
||||
import LottieIcon from '../LottieIcon';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
|
||||
interface AuthTypeModalProps {
|
||||
open: boolean;
|
||||
|
|
@ -42,10 +35,9 @@ const AuthTypeModal = ({
|
|||
const { license } = useAppSelector(state => state.config);
|
||||
|
||||
const [selected, setSelected] = useState<'file' | 'code'>(
|
||||
license.edition === 2 ? 'file' : 'code',
|
||||
);
|
||||
const [authVersion, setAuthVersion] = useState<'contributor' | 'enterprise'>(
|
||||
license.edition === 2 ? 'enterprise' : 'contributor',
|
||||
license.edition === ConstsLicenseEdition.LicenseEditionEnterprise
|
||||
? 'file'
|
||||
: 'code',
|
||||
);
|
||||
const [updateOpen, setUpdateOpen] = useState(false);
|
||||
const [code, setCode] = useState('');
|
||||
|
|
@ -53,16 +45,15 @@ const AuthTypeModal = ({
|
|||
const [file, setFile] = useState<File | undefined>(undefined);
|
||||
const [unbindLoading, setUnbindLoading] = useState(false);
|
||||
|
||||
const versionInfo = useVersionInfo();
|
||||
|
||||
const handleSubmit = () => {
|
||||
const params: PostApiV1LicensePayload = {
|
||||
license_edition: authVersion,
|
||||
setLoading(true);
|
||||
postApiV1License({
|
||||
license_type: selected,
|
||||
license_code: code,
|
||||
license_file: file,
|
||||
};
|
||||
setLoading(true);
|
||||
|
||||
postApiV1License(params)
|
||||
})
|
||||
.then(() => {
|
||||
message.success('激活成功');
|
||||
setUpdateOpen(false);
|
||||
|
|
@ -148,10 +139,8 @@ const AuthTypeModal = ({
|
|||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<Box sx={{ width: 120, flexShrink: 0 }}>产品型号</Box>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={2}>
|
||||
<Box sx={{ minWidth: 50 }}>
|
||||
{EditionType[license.edition as keyof typeof EditionType].text}
|
||||
</Box>
|
||||
{license.edition === 0 ? (
|
||||
<Box sx={{ minWidth: 50 }}>{versionInfo.label}</Box>
|
||||
{license.edition === ConstsLicenseEdition.LicenseEditionFree ? (
|
||||
<Stack direction={'row'} gap={2}>
|
||||
<Button
|
||||
size='small'
|
||||
|
|
@ -240,7 +229,7 @@ const AuthTypeModal = ({
|
|||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
{license.edition! > 0 && (
|
||||
{license.edition! !== ConstsLicenseEdition.LicenseEditionFree && (
|
||||
<Box>
|
||||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<Box sx={{ width: 120, flexShrink: 0 }}>授权时间</Box>
|
||||
|
|
@ -288,18 +277,6 @@ const AuthTypeModal = ({
|
|||
value={selected}
|
||||
change={(v: string) => setSelected(v as 'file' | 'code')}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
sx={{ mt: 2 }}
|
||||
value={authVersion}
|
||||
onChange={e =>
|
||||
setAuthVersion(e.target.value as 'contributor' | 'enterprise')
|
||||
}
|
||||
>
|
||||
<MenuItem value='contributor'>联创版</MenuItem>
|
||||
<MenuItem value='enterprise'>企业版</MenuItem>
|
||||
</TextField>
|
||||
{selected === 'code' && (
|
||||
<TextField
|
||||
sx={{ mt: 2 }}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,14 @@
|
|||
import HelpCenter from '@/assets/json/help-center.json';
|
||||
import IconUpgrade from '@/assets/json/upgrade.json';
|
||||
import LottieIcon from '@/components/LottieIcon';
|
||||
import { EditionType } from '@/constant/enums';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { Box, Stack, Tooltip } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import packageJson from '../../../package.json';
|
||||
import AuthTypeModal from './AuthTypeModal';
|
||||
import freeVersion from '@/assets/images/free-version.png';
|
||||
import enterpriseVersion from '@/assets/images/enterprise-version.png';
|
||||
import contributorVersion from '@/assets/images/contributor-version.png';
|
||||
|
||||
const versionMap = {
|
||||
0: freeVersion,
|
||||
1: contributorVersion,
|
||||
2: enterpriseVersion,
|
||||
};
|
||||
import { useVersionInfo } from '@/hooks';
|
||||
|
||||
const Version = () => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
const versionInfo = useVersionInfo();
|
||||
const curVersion = import.meta.env.VITE_APP_VERSION || packageJson.version;
|
||||
const [latestVersion, setLatestVersion] = useState<string | undefined>(
|
||||
undefined,
|
||||
|
|
@ -57,11 +47,8 @@ const Version = () => {
|
|||
>
|
||||
<Stack direction={'row'} alignItems='center' gap={0.5}>
|
||||
<Box sx={{ width: 30, color: 'text.tertiary' }}>型号</Box>
|
||||
<img
|
||||
src={versionMap[license.edition!]}
|
||||
style={{ height: 13, marginTop: -1 }}
|
||||
/>
|
||||
{EditionType[license.edition as keyof typeof EditionType].text}
|
||||
<img src={versionInfo.image} style={{ height: 13, marginTop: -1 }} />
|
||||
{versionInfo.label}
|
||||
</Stack>
|
||||
<Stack direction={'row'} gap={0.5}>
|
||||
<Box sx={{ width: 30, color: 'text.tertiary' }}>版本</Box>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import { Modal, message } from '@ctzhian/ui';
|
|||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import { ConstsUserKBPermission, V1KBUserInviteReq } from '@/request/types';
|
||||
import { ConstsLicenseEdition } from '@/request/pro/types';
|
||||
|
||||
|
|
@ -26,9 +27,13 @@ const VERSION_MAP = {
|
|||
message: '开源版只支持 1 个管理员',
|
||||
max: 1,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionContributor]: {
|
||||
message: '联创版最多支持 3 个管理员',
|
||||
max: 3,
|
||||
[ConstsLicenseEdition.LicenseEditionProfession]: {
|
||||
message: '专业版最多支持 20 个管理员',
|
||||
max: 20,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionBusiness]: {
|
||||
message: '商业版最多支持 50 个管理员',
|
||||
max: 50,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -45,9 +50,6 @@ const MemberAdd = ({
|
|||
const { kbList, license, refreshAdminRequest } = useAppSelector(
|
||||
state => state.config,
|
||||
);
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const {
|
||||
control,
|
||||
|
|
@ -118,6 +120,10 @@ const MemberAdd = ({
|
|||
});
|
||||
});
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
|
@ -253,6 +259,14 @@ const MemberAdd = ({
|
|||
fullWidth
|
||||
displayEmpty
|
||||
sx={{ height: 52 }}
|
||||
MenuProps={{
|
||||
sx: {
|
||||
'.Mui-disabled': {
|
||||
opacity: '1 !important',
|
||||
color: 'text.disabled',
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderValue={(value: V1KBUserInviteReq['perm']) => {
|
||||
return value ? (
|
||||
PERM_MAP[value]
|
||||
|
|
@ -266,17 +280,25 @@ const MemberAdd = ({
|
|||
>
|
||||
完全控制
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||
disabled={!isPro}
|
||||
>
|
||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
||||
文档管理{' '}
|
||||
<VersionCanUse
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
/>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||
disabled={!isPro}
|
||||
>
|
||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
||||
数据运营{' '}
|
||||
<VersionCanUse
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
/>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
import { styled } from '@mui/material';
|
||||
import { useVersionInfo } from '@/hooks';
|
||||
import { VersionInfoMap } from '@/constant/version';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
import { SxProps } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
const StyledMaskWrapper = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}));
|
||||
|
||||
const StyledMask = styled('div')(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
inset: -8,
|
||||
zIndex: 99,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flex: 1,
|
||||
borderRadius: '10px',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
background: 'rgba(241,242,248,0.8)',
|
||||
backdropFilter: 'blur(0.5px)',
|
||||
}));
|
||||
|
||||
const StyledMaskContent = styled('div')(({ theme }) => ({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
const StyledMaskVersion = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(0.5),
|
||||
padding: theme.spacing(0.5, 1),
|
||||
backgroundColor: theme.palette.background.paper3,
|
||||
borderRadius: '10px',
|
||||
fontSize: 12,
|
||||
lineHeight: 1,
|
||||
color: theme.palette.light.main,
|
||||
}));
|
||||
|
||||
const VersionMask = ({
|
||||
permission = [
|
||||
ConstsLicenseEdition.LicenseEditionFree,
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
],
|
||||
children,
|
||||
sx,
|
||||
}: {
|
||||
permission?: ConstsLicenseEdition[];
|
||||
children?: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
const versionInfo = useVersionInfo();
|
||||
const hasPermission = permission.includes(versionInfo.permission);
|
||||
if (hasPermission) return children;
|
||||
const nextVersionInfo = VersionInfoMap[permission[0]];
|
||||
|
||||
return (
|
||||
<StyledMaskWrapper>
|
||||
{children}
|
||||
<StyledMask sx={sx}>
|
||||
<StyledMaskContent>
|
||||
<StyledMaskVersion sx={{ backgroundColor: nextVersionInfo.bgColor }}>
|
||||
<img
|
||||
src={nextVersionInfo.image}
|
||||
style={{ width: 12, objectFit: 'contain', marginTop: 1 }}
|
||||
alt={nextVersionInfo.label}
|
||||
/>
|
||||
{nextVersionInfo?.label}可用
|
||||
</StyledMaskVersion>
|
||||
</StyledMaskContent>
|
||||
</StyledMask>
|
||||
</StyledMaskWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const VersionCanUse = ({
|
||||
permission = [
|
||||
ConstsLicenseEdition.LicenseEditionFree,
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
],
|
||||
sx,
|
||||
}: {
|
||||
permission?: ConstsLicenseEdition[];
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
const versionInfo = useVersionInfo();
|
||||
const hasPermission = permission.includes(versionInfo.permission);
|
||||
if (hasPermission) return null;
|
||||
const nextVersionInfo = VersionInfoMap[permission[0]];
|
||||
return (
|
||||
<StyledMaskContent sx={{ width: 'auto', ml: 1, ...sx }}>
|
||||
<StyledMaskVersion sx={{ backgroundColor: nextVersionInfo.bgColor }}>
|
||||
<img
|
||||
src={nextVersionInfo.image}
|
||||
style={{ width: 12, objectFit: 'contain', marginTop: 1 }}
|
||||
alt={nextVersionInfo.label}
|
||||
/>
|
||||
{nextVersionInfo?.label}可用
|
||||
</StyledMaskVersion>
|
||||
</StyledMaskContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default VersionMask;
|
||||
|
|
@ -797,21 +797,6 @@ export const FeedbackType = {
|
|||
3: '其他',
|
||||
};
|
||||
|
||||
export const Free = 0;
|
||||
export const Contributor = 1;
|
||||
export const Enterprise = 2;
|
||||
export const EditionType = {
|
||||
[Free]: {
|
||||
text: '开源版',
|
||||
},
|
||||
[Contributor]: {
|
||||
text: '联创版',
|
||||
},
|
||||
[Enterprise]: {
|
||||
text: '企业版',
|
||||
},
|
||||
};
|
||||
|
||||
export const DocWidth = {
|
||||
full: {
|
||||
label: '全屏',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,293 @@
|
|||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
|
||||
import freeVersion from '@/assets/images/free-version.png';
|
||||
import proVersion from '@/assets/images/pro-version.png';
|
||||
import businessVersion from '@/assets/images/business-version.png';
|
||||
import enterpriseVersion from '@/assets/images/enterprise-version.png';
|
||||
|
||||
export const PROFESSION_VERSION_PERMISSION = [
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
];
|
||||
|
||||
export const BUSINESS_VERSION_PERMISSION = [
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
];
|
||||
|
||||
export const ENTERPRISE_VERSION_PERMISSION = [
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
];
|
||||
|
||||
export const VersionInfoMap = {
|
||||
[ConstsLicenseEdition.LicenseEditionFree]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionFree,
|
||||
label: '开源版',
|
||||
image: freeVersion,
|
||||
bgColor: '#8E9DAC',
|
||||
nextVersion: ConstsLicenseEdition.LicenseEditionProfession,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionProfession]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionProfession,
|
||||
label: '专业版',
|
||||
image: proVersion,
|
||||
bgColor: '#0933BA',
|
||||
nextVersion: ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionBusiness]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
label: '商业版',
|
||||
image: businessVersion,
|
||||
bgColor: '#382A79',
|
||||
nextVersion: ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionEnterprise]: {
|
||||
permission: ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
label: '企业版',
|
||||
image: enterpriseVersion,
|
||||
bgColor: '#21222D',
|
||||
nextVersion: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 功能支持状态
|
||||
*/
|
||||
export enum FeatureStatus {
|
||||
/** 不支持 */
|
||||
NOT_SUPPORTED = 'not_supported',
|
||||
/** 支持 */
|
||||
SUPPORTED = 'supported',
|
||||
/** 基础配置 */
|
||||
BASIC = 'basic',
|
||||
/** 高级配置 */
|
||||
ADVANCED = 'advanced',
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本信息配置
|
||||
*/
|
||||
export interface VersionInfo {
|
||||
/** 版本名称 */
|
||||
label: string;
|
||||
/** 功能特性 */
|
||||
features: {
|
||||
/** Wiki 站点数量 */
|
||||
wikiCount: number;
|
||||
/** 每个 Wiki 的文档数量 */
|
||||
docCountPerWiki: number;
|
||||
/** 管理员数量 */
|
||||
adminCount: number;
|
||||
/** 管理员分权控制 */
|
||||
adminPermissionControl: FeatureStatus;
|
||||
/** SEO 配置 */
|
||||
seoConfig: FeatureStatus;
|
||||
/** 多语言支持 */
|
||||
multiLanguage: FeatureStatus;
|
||||
/** 自定义版权信息 */
|
||||
customCopyright: FeatureStatus;
|
||||
/** 访问流量分析 */
|
||||
trafficAnalysis: FeatureStatus;
|
||||
/** 自定义 AI 提示词 */
|
||||
customAIPrompt: FeatureStatus;
|
||||
/** SSO 登录 */
|
||||
ssoLogin: number;
|
||||
/** 访客权限控制 */
|
||||
visitorPermissionControl: FeatureStatus;
|
||||
/** 页面水印 */
|
||||
pageWatermark: FeatureStatus;
|
||||
/** 内容不可复制 */
|
||||
contentNoCopy: FeatureStatus;
|
||||
/** 敏感内容过滤 */
|
||||
sensitiveContentFilter: FeatureStatus;
|
||||
/** 网页挂件机器人 */
|
||||
webWidgetRobot: FeatureStatus;
|
||||
/** 飞书问答机器人 */
|
||||
feishuQARobot: FeatureStatus;
|
||||
/** 钉钉问答机器人 */
|
||||
dingtalkQARobot: FeatureStatus;
|
||||
/** 企业微信问答机器人 */
|
||||
wecomQARobot: FeatureStatus;
|
||||
/** 企业微信客服机器人 */
|
||||
wecomServiceRobot: FeatureStatus;
|
||||
/** Discord 问答机器人 */
|
||||
discordQARobot: FeatureStatus;
|
||||
/** 文档历史版本管理 */
|
||||
docVersionHistory: FeatureStatus;
|
||||
/** API 调用 */
|
||||
apiCall: FeatureStatus;
|
||||
/** 项目源码 */
|
||||
sourceCode: FeatureStatus;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本信息映射
|
||||
*/
|
||||
export const VERSION_INFO: Record<ConstsLicenseEdition, VersionInfo> = {
|
||||
[ConstsLicenseEdition.LicenseEditionFree]: {
|
||||
label: '开源版',
|
||||
features: {
|
||||
wikiCount: 1,
|
||||
docCountPerWiki: 300,
|
||||
adminCount: 1,
|
||||
adminPermissionControl: FeatureStatus.NOT_SUPPORTED,
|
||||
seoConfig: FeatureStatus.BASIC,
|
||||
multiLanguage: FeatureStatus.NOT_SUPPORTED,
|
||||
customCopyright: FeatureStatus.NOT_SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.BASIC,
|
||||
customAIPrompt: FeatureStatus.NOT_SUPPORTED,
|
||||
ssoLogin: 0,
|
||||
visitorPermissionControl: FeatureStatus.NOT_SUPPORTED,
|
||||
pageWatermark: FeatureStatus.NOT_SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.NOT_SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.NOT_SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.BASIC,
|
||||
feishuQARobot: FeatureStatus.BASIC,
|
||||
dingtalkQARobot: FeatureStatus.BASIC,
|
||||
wecomQARobot: FeatureStatus.BASIC,
|
||||
wecomServiceRobot: FeatureStatus.BASIC,
|
||||
discordQARobot: FeatureStatus.BASIC,
|
||||
docVersionHistory: FeatureStatus.NOT_SUPPORTED,
|
||||
apiCall: FeatureStatus.NOT_SUPPORTED,
|
||||
sourceCode: FeatureStatus.SUPPORTED,
|
||||
},
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionProfession]: {
|
||||
label: '专业版',
|
||||
features: {
|
||||
wikiCount: 10,
|
||||
docCountPerWiki: 10000,
|
||||
adminCount: 20,
|
||||
adminPermissionControl: FeatureStatus.SUPPORTED,
|
||||
seoConfig: FeatureStatus.ADVANCED,
|
||||
multiLanguage: FeatureStatus.SUPPORTED,
|
||||
customCopyright: FeatureStatus.SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.ADVANCED,
|
||||
customAIPrompt: FeatureStatus.SUPPORTED,
|
||||
ssoLogin: 0,
|
||||
visitorPermissionControl: FeatureStatus.NOT_SUPPORTED,
|
||||
pageWatermark: FeatureStatus.NOT_SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.NOT_SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.NOT_SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.ADVANCED,
|
||||
feishuQARobot: FeatureStatus.ADVANCED,
|
||||
dingtalkQARobot: FeatureStatus.ADVANCED,
|
||||
wecomQARobot: FeatureStatus.ADVANCED,
|
||||
wecomServiceRobot: FeatureStatus.ADVANCED,
|
||||
discordQARobot: FeatureStatus.ADVANCED,
|
||||
docVersionHistory: FeatureStatus.NOT_SUPPORTED,
|
||||
apiCall: FeatureStatus.NOT_SUPPORTED,
|
||||
sourceCode: FeatureStatus.NOT_SUPPORTED,
|
||||
},
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionBusiness]: {
|
||||
label: '商业版',
|
||||
features: {
|
||||
wikiCount: 20,
|
||||
docCountPerWiki: 10000,
|
||||
adminCount: 50,
|
||||
adminPermissionControl: FeatureStatus.SUPPORTED,
|
||||
seoConfig: FeatureStatus.ADVANCED,
|
||||
multiLanguage: FeatureStatus.SUPPORTED,
|
||||
customCopyright: FeatureStatus.SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.ADVANCED,
|
||||
customAIPrompt: FeatureStatus.SUPPORTED,
|
||||
ssoLogin: 2000,
|
||||
visitorPermissionControl: FeatureStatus.SUPPORTED,
|
||||
pageWatermark: FeatureStatus.SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.ADVANCED,
|
||||
feishuQARobot: FeatureStatus.ADVANCED,
|
||||
dingtalkQARobot: FeatureStatus.ADVANCED,
|
||||
wecomQARobot: FeatureStatus.ADVANCED,
|
||||
wecomServiceRobot: FeatureStatus.ADVANCED,
|
||||
discordQARobot: FeatureStatus.ADVANCED,
|
||||
docVersionHistory: FeatureStatus.SUPPORTED,
|
||||
apiCall: FeatureStatus.SUPPORTED,
|
||||
sourceCode: FeatureStatus.NOT_SUPPORTED,
|
||||
},
|
||||
},
|
||||
[ConstsLicenseEdition.LicenseEditionEnterprise]: {
|
||||
label: '企业版',
|
||||
features: {
|
||||
wikiCount: Infinity,
|
||||
docCountPerWiki: Infinity,
|
||||
adminCount: Infinity,
|
||||
adminPermissionControl: FeatureStatus.SUPPORTED,
|
||||
seoConfig: FeatureStatus.ADVANCED,
|
||||
multiLanguage: FeatureStatus.SUPPORTED,
|
||||
customCopyright: FeatureStatus.SUPPORTED,
|
||||
trafficAnalysis: FeatureStatus.ADVANCED,
|
||||
customAIPrompt: FeatureStatus.SUPPORTED,
|
||||
ssoLogin: Infinity,
|
||||
visitorPermissionControl: FeatureStatus.SUPPORTED,
|
||||
pageWatermark: FeatureStatus.SUPPORTED,
|
||||
contentNoCopy: FeatureStatus.SUPPORTED,
|
||||
sensitiveContentFilter: FeatureStatus.SUPPORTED,
|
||||
webWidgetRobot: FeatureStatus.ADVANCED,
|
||||
feishuQARobot: FeatureStatus.ADVANCED,
|
||||
dingtalkQARobot: FeatureStatus.ADVANCED,
|
||||
wecomQARobot: FeatureStatus.ADVANCED,
|
||||
wecomServiceRobot: FeatureStatus.ADVANCED,
|
||||
discordQARobot: FeatureStatus.ADVANCED,
|
||||
docVersionHistory: FeatureStatus.SUPPORTED,
|
||||
apiCall: FeatureStatus.SUPPORTED,
|
||||
sourceCode: FeatureStatus.SUPPORTED,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 功能特性标签映射
|
||||
*/
|
||||
export const FEATURE_LABELS: Record<string, string> = {
|
||||
wikiCount: 'Wiki 站点数量',
|
||||
docCountPerWiki: '每个 Wiki 的文档数量',
|
||||
adminCount: '管理员数量',
|
||||
adminPermissionControl: '管理员分权控制',
|
||||
seoConfig: 'SEO 配置',
|
||||
multiLanguage: '多语言支持',
|
||||
customCopyright: '自定义版权信息',
|
||||
trafficAnalysis: '访问流量分析',
|
||||
customAIPrompt: '自定义 AI 提示词',
|
||||
ssoLogin: 'SSO 登录',
|
||||
visitorPermissionControl: '访客权限控制',
|
||||
pageWatermark: '页面水印',
|
||||
contentNoCopy: '内容不可复制',
|
||||
sensitiveContentFilter: '敏感内容过滤',
|
||||
webWidgetRobot: '网页挂件机器人',
|
||||
feishuQARobot: '飞书问答机器人',
|
||||
dingtalkQARobot: '钉钉问答机器人',
|
||||
wecomQARobot: '企业微信问答机器人',
|
||||
wecomServiceRobot: '企业微信客服机器人',
|
||||
discordQARobot: 'Discord 问答机器人',
|
||||
docVersionHistory: '文档历史版本管理',
|
||||
apiCall: 'API 调用',
|
||||
sourceCode: '项目源码',
|
||||
};
|
||||
|
||||
/**
|
||||
* 功能状态显示文本映射
|
||||
*/
|
||||
export const FEATURE_STATUS_LABELS: Record<FeatureStatus, string> = {
|
||||
[FeatureStatus.NOT_SUPPORTED]: '不支持',
|
||||
[FeatureStatus.SUPPORTED]: '支持',
|
||||
[FeatureStatus.BASIC]: '基础配置',
|
||||
[FeatureStatus.ADVANCED]: '高级配置',
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取功能特性值
|
||||
*/
|
||||
export function getFeatureValue<K extends keyof VersionInfo['features']>(
|
||||
edition: ConstsLicenseEdition,
|
||||
key: K,
|
||||
): VersionInfo['features'][K] {
|
||||
return (
|
||||
VERSION_INFO[edition] ||
|
||||
VERSION_INFO[ConstsLicenseEdition.LicenseEditionFree]
|
||||
).features[key];
|
||||
}
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
export { useBindCaptcha } from './useBindCaptcha';
|
||||
export { useCommitPendingInput } from './useCommitPendingInput';
|
||||
export { useURLSearchParams } from './useURLSearchParams';
|
||||
export {
|
||||
useFeatureValue,
|
||||
useFeatureValueSupported,
|
||||
useVersionInfo,
|
||||
} from './useVersionFeature';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
FeatureStatus,
|
||||
VersionInfoMap,
|
||||
VersionInfo,
|
||||
getFeatureValue,
|
||||
} from '@/constant/version';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
export const useFeatureValue = <K extends keyof VersionInfo['features']>(
|
||||
key: K,
|
||||
): VersionInfo['features'][K] => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
return getFeatureValue(license.edition!, key);
|
||||
};
|
||||
|
||||
export const useFeatureValueSupported = (
|
||||
key: keyof VersionInfo['features'],
|
||||
) => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
return (
|
||||
getFeatureValue(license.edition!, key) === FeatureStatus.SUPPORTED ||
|
||||
getFeatureValue(license.edition!, key) === FeatureStatus.ADVANCED
|
||||
);
|
||||
};
|
||||
|
||||
export const useVersionInfo = () => {
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
return (
|
||||
VersionInfoMap[
|
||||
license.edition ?? ConstsLicenseEdition.LicenseEditionFree
|
||||
] || VersionInfoMap[ConstsLicenseEdition.LicenseEditionFree]
|
||||
);
|
||||
};
|
||||
|
|
@ -8,6 +8,8 @@ import { styled } from '@mui/material/styles';
|
|||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
import DocModal from './DocModal';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
import { useURLSearchParams } from '@/hooks';
|
||||
import {
|
||||
|
|
@ -46,7 +48,7 @@ const statusColorMap = {
|
|||
} as const;
|
||||
|
||||
export default function ContributionPage() {
|
||||
const { kb_id = '', kbDetail } = useAppSelector(state => state.config);
|
||||
const { kb_id = '', license } = useAppSelector(state => state.config);
|
||||
const [searchParams, setSearchParams] = useURLSearchParams();
|
||||
const page = Number(searchParams.get('page') || '1');
|
||||
const pageSize = Number(searchParams.get('page_size') || '20');
|
||||
|
|
@ -283,111 +285,114 @@ export default function ContributionPage() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (kb_id) getData();
|
||||
if (kb_id && PROFESSION_VERSION_PERMISSION.includes(license.edition!))
|
||||
getData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, pageSize, nodeNameParam, authNameParam, kb_id]);
|
||||
}, [page, pageSize, nodeNameParam, authNameParam, kb_id, license.edition]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
sx={{ p: 2 }}
|
||||
>
|
||||
<StyledSearchRow direction='row' sx={{ p: 0, flex: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='文档'
|
||||
value={searchDoc}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ node_name: searchDoc || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ node_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchDoc(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='用户'
|
||||
value={searchUser}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ auth_name: searchUser || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ auth_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchUser(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
</StyledSearchRow>
|
||||
</Stack>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey='id'
|
||||
height='calc(100vh - 148px)'
|
||||
size='small'
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
...tableSx,
|
||||
'.MuiTableContainer-root': {
|
||||
height: 'calc(100vh - 148px - 70px)',
|
||||
},
|
||||
}}
|
||||
pagination={{
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
onChange: (page, pageSize) => {
|
||||
setSearchParams({
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
});
|
||||
},
|
||||
}}
|
||||
PaginationProps={{
|
||||
sx: {
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
p: 2,
|
||||
'.MuiSelect-root': {
|
||||
width: 100,
|
||||
<VersionMask permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
sx={{ p: 2 }}
|
||||
>
|
||||
<StyledSearchRow direction='row' sx={{ p: 0, flex: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='文档'
|
||||
value={searchDoc}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ node_name: searchDoc || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ node_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchDoc(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
size='small'
|
||||
label='用户'
|
||||
value={searchUser}
|
||||
onKeyUp={e => {
|
||||
if (e.key === 'Enter') {
|
||||
setSearchParams({ auth_name: searchUser || '', page: '1' });
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
setSearchParams({ auth_name: e.target.value, page: '1' });
|
||||
}}
|
||||
onChange={e => setSearchUser(e.target.value)}
|
||||
sx={{ width: 200 }}
|
||||
/>
|
||||
</StyledSearchRow>
|
||||
</Stack>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
rowKey='id'
|
||||
height='calc(100vh - 148px)'
|
||||
size='small'
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
...tableSx,
|
||||
'.MuiTableContainer-root': {
|
||||
height: 'calc(100vh - 148px - 70px)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
pagination={{
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
onChange: (page, pageSize) => {
|
||||
setSearchParams({
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
});
|
||||
},
|
||||
}}
|
||||
PaginationProps={{
|
||||
sx: {
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
p: 2,
|
||||
'.MuiSelect-root': {
|
||||
width: 100,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{previewRow?.meta?.content_type === 'md' ? (
|
||||
<MarkdownPreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
{previewRow?.meta?.content_type === 'md' ? (
|
||||
<MarkdownPreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
) : (
|
||||
<ContributePreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
)}
|
||||
<DocModal
|
||||
open={docModalOpen}
|
||||
onClose={() => setDocModalOpen(false)}
|
||||
onOk={handleDocModalOk}
|
||||
/>
|
||||
) : (
|
||||
<ContributePreviewModal
|
||||
open={open}
|
||||
row={previewRow}
|
||||
onClose={closeDialog}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
)}
|
||||
<DocModal
|
||||
open={docModalOpen}
|
||||
onClose={() => setDocModalOpen(false)}
|
||||
onOk={handleDocModalOk}
|
||||
/>
|
||||
</VersionMask>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import { ChatConversationPair } from '@/api';
|
|||
import { getApiV1ConversationDetail } from '@/request/Conversation';
|
||||
import { DomainConversationDetailResp } from '@/request/types';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import Card from '@/components/Card';
|
||||
import MarkDown from '@/components/MarkDown';
|
||||
import { useAppSelector } from '@/store';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
|
|
@ -13,10 +13,169 @@ import {
|
|||
Box,
|
||||
Stack,
|
||||
useTheme,
|
||||
styled,
|
||||
alpha,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Ellipsis, Icon, Modal } from '@ctzhian/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const handleThinkingContent = (content: string) => {
|
||||
const thinkRegex = /<think>([\s\S]*?)(?:<\/think>|$)/g;
|
||||
const thinkMatches = [];
|
||||
let match;
|
||||
while ((match = thinkRegex.exec(content)) !== null) {
|
||||
thinkMatches.push(match[1]);
|
||||
}
|
||||
|
||||
let answerContent = content.replace(/<think>[\s\S]*?<\/think>/g, '');
|
||||
answerContent = answerContent.replace(/<think>[\s\S]*$/, '');
|
||||
|
||||
return {
|
||||
thinkingContent: thinkMatches.join(''),
|
||||
answerContent: answerContent,
|
||||
};
|
||||
};
|
||||
|
||||
export const StyledConversationItem = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
}));
|
||||
|
||||
// 聊天气泡相关组件
|
||||
export const StyledUserBubble = styled(Box)(({ theme }) => ({
|
||||
alignSelf: 'flex-end',
|
||||
maxWidth: '75%',
|
||||
padding: theme.spacing(1, 2),
|
||||
borderRadius: '10px 10px 0px 10px',
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
fontSize: 14,
|
||||
wordBreak: 'break-word',
|
||||
}));
|
||||
|
||||
export const StyledAiBubble = styled(Box)(({ theme }) => ({
|
||||
alignSelf: 'flex-start',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: theme.spacing(3),
|
||||
}));
|
||||
|
||||
export const StyledAiBubbleContent = styled(Box)(() => ({
|
||||
wordBreak: 'break-word',
|
||||
}));
|
||||
|
||||
// 对话相关组件
|
||||
export const StyledAccordion = styled(Accordion)(() => ({
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
'&:before': {
|
||||
content: '""',
|
||||
height: 0,
|
||||
},
|
||||
background: 'transparent',
|
||||
backgroundImage: 'none',
|
||||
}));
|
||||
|
||||
export const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
userSelect: 'text',
|
||||
borderRadius: '10px',
|
||||
backgroundColor: theme.palette.background.paper3,
|
||||
border: '1px solid',
|
||||
borderColor: theme.palette.divider,
|
||||
}));
|
||||
|
||||
export const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
borderTop: 'none',
|
||||
}));
|
||||
|
||||
export const StyledQuestionText = styled(Box)(() => ({
|
||||
fontWeight: '700',
|
||||
fontSize: 16,
|
||||
lineHeight: '24px',
|
||||
wordBreak: 'break-all',
|
||||
}));
|
||||
|
||||
// 搜索结果相关组件
|
||||
export const StyledChunkAccordion = styled(Accordion)(({ theme }) => ({
|
||||
backgroundImage: 'none',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
}));
|
||||
|
||||
export const StyledChunkAccordionSummary = styled(AccordionSummary)(
|
||||
({ theme }) => ({
|
||||
justifyContent: 'flex-start',
|
||||
gap: theme.spacing(2),
|
||||
'.MuiAccordionSummary-content': {
|
||||
flexGrow: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const StyledChunkAccordionDetails = styled(AccordionDetails)(
|
||||
({ theme }) => ({
|
||||
paddingTop: 0,
|
||||
paddingLeft: theme.spacing(2),
|
||||
borderTop: 'none',
|
||||
borderLeft: '1px solid',
|
||||
borderColor: theme.palette.divider,
|
||||
}),
|
||||
);
|
||||
|
||||
export const StyledChunkItem = styled(Box)(({ theme }) => ({
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
'.hover-primary': {
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// 思考过程相关组件
|
||||
export const StyledThinkingAccordion = styled(Accordion)(({ theme }) => ({
|
||||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
paddingBottom: theme.spacing(2),
|
||||
'&:before': {
|
||||
content: '""',
|
||||
height: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
export const StyledThinkingAccordionSummary = styled(AccordionSummary)(
|
||||
({ theme }) => ({
|
||||
justifyContent: 'flex-start',
|
||||
gap: theme.spacing(2),
|
||||
'.MuiAccordionSummary-content': {
|
||||
flexGrow: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const StyledThinkingAccordionDetails = styled(AccordionDetails)(
|
||||
({ theme }) => ({
|
||||
paddingTop: 0,
|
||||
paddingLeft: theme.spacing(2),
|
||||
borderTop: 'none',
|
||||
borderLeft: '1px solid',
|
||||
borderColor: theme.palette.divider,
|
||||
'.markdown-body': {
|
||||
opacity: 0.75,
|
||||
fontSize: 12,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const Detail = ({
|
||||
id,
|
||||
open,
|
||||
|
|
@ -55,7 +214,11 @@ const Detail = ({
|
|||
};
|
||||
} else if (message.role === 'assistant') {
|
||||
if (currentPair.user) {
|
||||
currentPair.assistant = message.content;
|
||||
const { thinkingContent, answerContent } = handleThinkingContent(
|
||||
message.content || '',
|
||||
);
|
||||
currentPair.assistant = answerContent;
|
||||
currentPair.thinking_content = thinkingContent;
|
||||
currentPair.created_at = message.created_at;
|
||||
// @ts-expect-error 类型不兼容
|
||||
currentPair.info = message.info;
|
||||
|
|
@ -167,26 +330,43 @@ const Detail = ({
|
|||
<Stack gap={2}>
|
||||
{conversations &&
|
||||
conversations.map((item, index) => (
|
||||
<Box key={index}>
|
||||
<Accordion defaultExpanded={true}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 24 }} />}
|
||||
sx={{
|
||||
userSelect: 'text',
|
||||
backgroundColor: 'background.paper3',
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{item.user}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<MarkDown
|
||||
content={item.assistant || '未查询到回答内容'}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Box>
|
||||
<StyledConversationItem key={index}>
|
||||
{/* 用户问题气泡 - 右对齐 */}
|
||||
<StyledUserBubble>{item.user}</StyledUserBubble>
|
||||
|
||||
{/* AI回答气泡 - 左对齐 */}
|
||||
<StyledAiBubble>
|
||||
{/* 思考过程 */}
|
||||
{!!item.thinking_content && (
|
||||
<StyledThinkingAccordion defaultExpanded>
|
||||
<StyledThinkingAccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
<Typography
|
||||
variant='body2'
|
||||
sx={theme => ({
|
||||
fontSize: 12,
|
||||
color: alpha(theme.palette.text.primary, 0.5),
|
||||
})}
|
||||
>
|
||||
已思考
|
||||
</Typography>
|
||||
</Stack>
|
||||
</StyledThinkingAccordionSummary>
|
||||
|
||||
<StyledThinkingAccordionDetails>
|
||||
<MarkDown content={item.thinking_content || ''} />
|
||||
</StyledThinkingAccordionDetails>
|
||||
</StyledThinkingAccordion>
|
||||
)}
|
||||
|
||||
{/* AI回答内容 */}
|
||||
<StyledAiBubbleContent>
|
||||
<MarkDown content={item.assistant} />
|
||||
</StyledAiBubbleContent>
|
||||
</StyledAiBubble>
|
||||
</StyledConversationItem>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import {
|
|||
import dayjs from 'dayjs';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
|
||||
interface DocPropertiesModalProps {
|
||||
open: boolean;
|
||||
|
|
@ -40,8 +42,6 @@ interface DocPropertiesModalProps {
|
|||
data: DomainNodeListItemResp[];
|
||||
}
|
||||
|
||||
const tips = '(企业版可用)';
|
||||
|
||||
const StyledText = styled('div')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 16,
|
||||
|
|
@ -53,7 +53,12 @@ const PER_OPTIONS = [
|
|||
value: ConstsNodeAccessPerm.NodeAccessPermOpen,
|
||||
},
|
||||
{
|
||||
label: '部分开放',
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<span>部分开放</span>
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: ConstsNodeAccessPerm.NodeAccessPermPartial,
|
||||
},
|
||||
{
|
||||
|
|
@ -128,13 +133,13 @@ const DocPropertiesModal = ({
|
|||
visitable: values.visitable as ConstsNodeAccessPerm,
|
||||
visible: values.visible as ConstsNodeAccessPerm,
|
||||
},
|
||||
answerable_groups: isEnterprise
|
||||
answerable_groups: isBusiness
|
||||
? values.answerable_groups.map(item => item.id!)
|
||||
: undefined,
|
||||
visitable_groups: isEnterprise
|
||||
visitable_groups: isBusiness
|
||||
? values.visitable_groups.map(item => item.id!)
|
||||
: undefined,
|
||||
visible_groups: isEnterprise
|
||||
visible_groups: isBusiness
|
||||
? values.visible_groups.map(item => item.id!)
|
||||
: undefined,
|
||||
}),
|
||||
|
|
@ -153,15 +158,15 @@ const DocPropertiesModal = ({
|
|||
});
|
||||
});
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const tree = filterEmptyFolders(convertToTree(data));
|
||||
|
||||
useEffect(() => {
|
||||
if (open && data) {
|
||||
if (isEnterprise) {
|
||||
if (isBusiness) {
|
||||
getApiProV1AuthGroupList({
|
||||
kb_id: kb_id!,
|
||||
page: 1,
|
||||
|
|
@ -206,7 +211,7 @@ const DocPropertiesModal = ({
|
|||
);
|
||||
});
|
||||
}
|
||||
}, [open, data, isEnterprise]);
|
||||
}, [open, data, isBusiness]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
|
|
@ -302,22 +307,15 @@ const DocPropertiesModal = ({
|
|||
name='answerable'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup row {...field}>
|
||||
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||
{PER_OPTIONS.map(option => (
|
||||
<FormControlLabel
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
option.label +
|
||||
(!isEnterprise &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
? tips
|
||||
: '')
|
||||
}
|
||||
label={option.label}
|
||||
disabled={
|
||||
!isEnterprise &&
|
||||
!isBusiness &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
}
|
||||
|
|
@ -359,22 +357,15 @@ const DocPropertiesModal = ({
|
|||
name='visitable'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup row {...field}>
|
||||
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||
{PER_OPTIONS.map(option => (
|
||||
<FormControlLabel
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
option.label +
|
||||
(!isEnterprise &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
? tips
|
||||
: '')
|
||||
}
|
||||
label={option.label}
|
||||
disabled={
|
||||
!isEnterprise &&
|
||||
!isBusiness &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
}
|
||||
|
|
@ -416,22 +407,15 @@ const DocPropertiesModal = ({
|
|||
name='visible'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup row {...field}>
|
||||
<RadioGroup row {...field} sx={{ gap: 2 }}>
|
||||
{PER_OPTIONS.map(option => (
|
||||
<FormControlLabel
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
option.label +
|
||||
(!isEnterprise &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
? tips
|
||||
: '')
|
||||
}
|
||||
label={option.label}
|
||||
disabled={
|
||||
!isEnterprise &&
|
||||
!isBusiness &&
|
||||
option.value ===
|
||||
ConstsNodeAccessPerm.NodeAccessPermPartial
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import { useNavigate, useOutletContext } from 'react-router-dom';
|
|||
import { WrapContext } from '..';
|
||||
import DocAddByCustomText from '../../component/DocAddByCustomText';
|
||||
import DocDelete from '../../component/DocDelete';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
|
||||
interface HeaderProps {
|
||||
edit: boolean;
|
||||
|
|
@ -52,8 +54,8 @@ const Header = ({
|
|||
|
||||
const [showSaveTip, setShowSaveTip] = useState(false);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const handlePublish = useCallback(() => {
|
||||
|
|
@ -309,6 +311,7 @@ const Header = ({
|
|||
// },
|
||||
{
|
||||
key: 'copy',
|
||||
textSx: { flex: 1 },
|
||||
label: <StyledMenuSelect>复制</StyledMenuSelect>,
|
||||
onClick: () => {
|
||||
if (kb_id) {
|
||||
|
|
@ -328,26 +331,22 @@ const Header = ({
|
|||
},
|
||||
{
|
||||
key: 'version',
|
||||
textSx: { flex: 1 },
|
||||
label: (
|
||||
<StyledMenuSelect disabled={!isEnterprise}>
|
||||
历史版本{' '}
|
||||
{!isEnterprise && (
|
||||
<Tooltip title='企业版可用' placement='top' arrow>
|
||||
<InfoIcon
|
||||
sx={{ color: 'text.secondary', fontSize: 14 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<StyledMenuSelect disabled={!isBusiness}>
|
||||
历史版本
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</StyledMenuSelect>
|
||||
),
|
||||
onClick: () => {
|
||||
if (isEnterprise) {
|
||||
if (isBusiness) {
|
||||
navigate(`/doc/editor/history/${detail.id}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'rename',
|
||||
textSx: { flex: 1 },
|
||||
label: <StyledMenuSelect>重命名</StyledMenuSelect>,
|
||||
onClick: () => {
|
||||
setRenameOpen(true);
|
||||
|
|
@ -355,6 +354,7 @@ const Header = ({
|
|||
},
|
||||
{
|
||||
key: 'delete',
|
||||
textSx: { flex: 1 },
|
||||
label: <StyledMenuSelect>删除</StyledMenuSelect>,
|
||||
onClick: () => {
|
||||
setDelOpen(true);
|
||||
|
|
@ -566,7 +566,7 @@ const StyledMenuSelect = styled('div')<{ disabled?: boolean }>(
|
|||
padding: theme.spacing(0, 2),
|
||||
lineHeight: '40px',
|
||||
height: 40,
|
||||
width: 106,
|
||||
minWidth: 106,
|
||||
borderRadius: '5px',
|
||||
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import Header from './Header';
|
|||
import Summary from './Summary';
|
||||
import Toc from './Toc';
|
||||
import Toolbar from './Toolbar';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface WrapProps {
|
||||
detail: V1NodeDetailResp;
|
||||
|
|
@ -72,8 +73,8 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
|||
emoji: defaultDetail.meta?.emoji || '',
|
||||
});
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const debouncedUpdateSummary = useCallback(
|
||||
|
|
@ -383,7 +384,7 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
|||
</Stack>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip arrow title={isEnterprise ? '查看历史版本' : ''}>
|
||||
<Tooltip arrow title={isBusiness ? '查看历史版本' : ''}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
|
|
@ -391,13 +392,13 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
|
|||
sx={{
|
||||
fontSize: 12,
|
||||
color: 'text.tertiary',
|
||||
cursor: isEnterprise ? 'pointer' : 'text',
|
||||
cursor: isBusiness ? 'pointer' : 'text',
|
||||
':hover': {
|
||||
color: isEnterprise ? 'primary.main' : 'text.tertiary',
|
||||
color: isBusiness ? 'primary.main' : 'text.tertiary',
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isEnterprise) {
|
||||
if (isBusiness) {
|
||||
navigate(`/doc/editor/history/${defaultDetail.id}`);
|
||||
}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
IconButton,
|
||||
Stack,
|
||||
useTheme,
|
||||
ButtonBase,
|
||||
} from '@mui/material';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import VersionPublish from '../release/components/VersionPublish';
|
||||
|
|
@ -418,15 +419,19 @@ const Content = () => {
|
|||
>
|
||||
{publish.unpublished} 个 文档/文件夹未发布,
|
||||
</Box>
|
||||
<Button
|
||||
size='small'
|
||||
sx={{ minWidth: 0, p: 0, fontSize: 12 }}
|
||||
<ButtonBase
|
||||
disableRipple
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
color: 'primary.main',
|
||||
}}
|
||||
onClick={() => {
|
||||
setPublishOpen(true);
|
||||
}}
|
||||
>
|
||||
去发布
|
||||
</Button>
|
||||
</ButtonBase>
|
||||
</>
|
||||
)}
|
||||
{ragReStartCount > 0 && (
|
||||
|
|
@ -441,15 +446,19 @@ const Content = () => {
|
|||
>
|
||||
{ragReStartCount} 个文档未学习,
|
||||
</Box>
|
||||
<Button
|
||||
size='small'
|
||||
sx={{ minWidth: 0, p: 0, fontSize: 12 }}
|
||||
<ButtonBase
|
||||
disableRipple
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
color: 'primary.main',
|
||||
}}
|
||||
onClick={() => {
|
||||
setRagOpen(true);
|
||||
}}
|
||||
>
|
||||
去学习
|
||||
</Button>
|
||||
</ButtonBase>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
ButtonBase,
|
||||
} from '@mui/material';
|
||||
import { Ellipsis, Table, Modal, Icon, message } from '@ctzhian/ui';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
|
||||
|
|
@ -162,8 +163,8 @@ const Comments = ({
|
|||
useState<DomainWebAppCommentSettings | null>(null);
|
||||
|
||||
const isEnableReview = useMemo(() => {
|
||||
return !!(license.edition === 1 || license.edition === 2);
|
||||
}, [license]);
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowCommentsFilter(isEnableReview);
|
||||
|
|
|
|||
|
|
@ -3,14 +3,18 @@ import { getApiV1ConversationMessageDetail } from '@/request';
|
|||
import MarkDown from '@/components/MarkDown';
|
||||
import { useAppSelector } from '@/store';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import { Ellipsis, Icon, Modal } from '@ctzhian/ui';
|
||||
import { Box, Stack, Typography, alpha } from '@mui/material';
|
||||
import { Ellipsis, Modal } from '@ctzhian/ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
StyledConversationItem,
|
||||
StyledUserBubble,
|
||||
StyledAiBubble,
|
||||
StyledThinkingAccordion,
|
||||
StyledThinkingAccordionSummary,
|
||||
StyledThinkingAccordionDetails,
|
||||
StyledAiBubbleContent,
|
||||
} from '../conversation/Detail';
|
||||
|
||||
const Detail = ({
|
||||
id,
|
||||
|
|
@ -36,6 +40,7 @@ const Detail = ({
|
|||
user: data.question,
|
||||
assistant: res.content!,
|
||||
created_at: res.created_at!,
|
||||
thinking_content: '',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -62,24 +67,43 @@ const Detail = ({
|
|||
>
|
||||
<Box sx={{ fontSize: 14 }}>
|
||||
<Box>
|
||||
<Accordion defaultExpanded={true}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 24 }} />}
|
||||
sx={{
|
||||
userSelect: 'text',
|
||||
backgroundColor: 'background.paper3',
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{conversations?.user}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<MarkDown
|
||||
content={conversations?.assistant || '未查询到回答内容'}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<StyledConversationItem>
|
||||
{/* 用户问题气泡 - 右对齐 */}
|
||||
<StyledUserBubble>{conversations?.user}</StyledUserBubble>
|
||||
|
||||
{/* AI回答气泡 - 左对齐 */}
|
||||
<StyledAiBubble>
|
||||
{/* 思考过程 */}
|
||||
{!!conversations?.thinking_content && (
|
||||
<StyledThinkingAccordion defaultExpanded>
|
||||
<StyledThinkingAccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
<Typography
|
||||
variant='body2'
|
||||
sx={theme => ({
|
||||
fontSize: 12,
|
||||
color: alpha(theme.palette.text.primary, 0.5),
|
||||
})}
|
||||
>
|
||||
已思考
|
||||
</Typography>
|
||||
</Stack>
|
||||
</StyledThinkingAccordionSummary>
|
||||
|
||||
<StyledThinkingAccordionDetails>
|
||||
<MarkDown content={conversations?.thinking_content || ''} />
|
||||
</StyledThinkingAccordionDetails>
|
||||
</StyledThinkingAccordion>
|
||||
)}
|
||||
|
||||
{/* AI回答内容 */}
|
||||
<StyledAiBubbleContent>
|
||||
<MarkDown content={conversations?.assistant || ''} />
|
||||
</StyledAiBubbleContent>
|
||||
</StyledAiBubble>
|
||||
</StyledConversationItem>
|
||||
</Box>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import dayjs from 'dayjs';
|
|||
import { ColumnType } from '@ctzhian/ui/dist/Table';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface AddRoleProps {
|
||||
open: boolean;
|
||||
|
|
@ -23,7 +25,8 @@ interface AddRoleProps {
|
|||
}
|
||||
|
||||
const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
||||
const { kb_id, license } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
const [list, setList] = useState<V1UserListItemResp[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string>('');
|
||||
|
|
@ -31,10 +34,6 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
|||
ConstsUserKBPermission.UserKBPermissionFullControl,
|
||||
);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const columns: ColumnType<V1UserListItemResp>[] = [
|
||||
{
|
||||
title: '',
|
||||
|
|
@ -119,6 +118,10 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
|||
}
|
||||
}, [open]);
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title='添加 Wiki 站管理员'
|
||||
|
|
@ -209,22 +212,33 @@ const AddRole = ({ open, onCancel, onOk, selectedIds }: AddRoleProps) => {
|
|||
fullWidth
|
||||
sx={{ height: 52 }}
|
||||
value={perm}
|
||||
MenuProps={{
|
||||
sx: {
|
||||
'.Mui-disabled': {
|
||||
opacity: '1 !important',
|
||||
color: 'text.disabled',
|
||||
},
|
||||
},
|
||||
}}
|
||||
onChange={e => setPerm(e.target.value as V1KBUserInviteReq['perm'])}
|
||||
>
|
||||
<MenuItem value={ConstsUserKBPermission.UserKBPermissionFullControl}>
|
||||
完全控制
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||
disabled={!isPro}
|
||||
>
|
||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
||||
文档管理{' '}
|
||||
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||
disabled={!isPro}
|
||||
>
|
||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
||||
数据运营{' '}
|
||||
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormItem>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { getApiProV1Prompt, postApiProV1Prompt } from '@/request/pro/Prompt';
|
||||
import { DomainKnowledgeBaseDetail } from '@/request/types';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { Box, Slider, TextField } from '@mui/material';
|
||||
|
|
@ -33,11 +34,12 @@ const CardAI = ({ kb }: CardAIProps) => {
|
|||
});
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return license.edition === 1 || license.edition === 2;
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb.id || !isPro) return;
|
||||
if (!kb.id || !PROFESSION_VERSION_PERMISSION.includes(license.edition!))
|
||||
return;
|
||||
getApiProV1Prompt({ kb_id: kb.id! }).then(res => {
|
||||
setValue('content', res.content || '');
|
||||
});
|
||||
|
|
@ -54,7 +56,7 @@ const CardAI = ({ kb }: CardAIProps) => {
|
|||
<SettingCardItem title='智能问答' isEdit={isEdit} onSubmit={onSubmit}>
|
||||
<FormItem
|
||||
vertical
|
||||
tooltip={!isPro && '联创版和企业版可用'}
|
||||
permission={PROFESSION_VERSION_PERMISSION}
|
||||
extra={
|
||||
<Box
|
||||
sx={{
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import { ColumnType } from '@ctzhian/ui/dist/Table';
|
|||
import { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import { SettingCardItem, FormItem } from './Common';
|
||||
|
||||
interface CardAuthProps {
|
||||
|
|
@ -114,7 +116,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
}),
|
||||
value.enabled === '2' &&
|
||||
source_type !== EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword
|
||||
? isPro
|
||||
? isBusiness
|
||||
? postApiProV1AuthSet({
|
||||
kb_id,
|
||||
source_type: value.source_type as ConstsSourceType,
|
||||
|
|
@ -157,25 +159,18 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
});
|
||||
});
|
||||
|
||||
const isPro = useMemo(() => {
|
||||
return license.edition === 1 || license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
useEffect(() => {
|
||||
const source_type = isPro
|
||||
const source_type = isBusiness
|
||||
? kb.access_settings?.source_type ||
|
||||
EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword
|
||||
: kb.access_settings?.source_type ===
|
||||
EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub
|
||||
? EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub
|
||||
: EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword;
|
||||
: EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword;
|
||||
setValue('source_type', source_type);
|
||||
sourceTypeRef.current = source_type;
|
||||
}, [kb, isPro]);
|
||||
}, [kb, isBusiness]);
|
||||
|
||||
useEffect(() => {
|
||||
if (kb.access_settings?.simple_auth) {
|
||||
|
|
@ -191,7 +186,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
}, [kb]);
|
||||
|
||||
const getAuth = () => {
|
||||
if (isPro) {
|
||||
if (isBusiness) {
|
||||
getApiProV1AuthGet({
|
||||
kb_id,
|
||||
source_type: source_type as ConstsSourceType,
|
||||
|
|
@ -236,7 +231,7 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
useEffect(() => {
|
||||
if (!kb_id || enabled !== '2') return;
|
||||
getAuth();
|
||||
}, [kb_id, isPro, source_type, enabled]);
|
||||
}, [kb_id, isBusiness, source_type, enabled]);
|
||||
|
||||
const columns: ColumnType<GithubComChaitinPandaWikiProApiAuthV1AuthItem>[] = [
|
||||
{
|
||||
|
|
@ -875,8 +870,18 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
MenuProps={{
|
||||
sx: {
|
||||
'.Mui-disabled': {
|
||||
opacity: '1 !important',
|
||||
color: 'text.disabled',
|
||||
},
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
sx={{ height: 52 }}
|
||||
sx={{
|
||||
height: 52,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypePassword}
|
||||
|
|
@ -885,44 +890,52 @@ const CardAuth = ({ kb, refresh }: CardAuthProps) => {
|
|||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeDingTalk}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
钉钉登录 {isPro ? '' : tips}
|
||||
钉钉登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeFeishu}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
飞书登录 {isPro ? '' : tips}
|
||||
飞书登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeWeCom}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
企业微信登录 {isPro ? '' : tips}
|
||||
企业微信登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeOAuth}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
OAuth 登录 {isPro ? '' : tips}
|
||||
OAuth 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeCAS}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
CAS 登录 {isPro ? '' : tips}
|
||||
CAS 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeLDAP}
|
||||
disabled={!isPro}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
LDAP 登录 {isPro ? '' : tips}
|
||||
LDAP 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value={EXTEND_CONSTS_SOURCE_TYPE.SourceTypeGitHub}
|
||||
disabled={!isBusiness}
|
||||
>
|
||||
GitHub 登录
|
||||
GitHub 登录{' '}
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
DomainKnowledgeBaseDetail,
|
||||
} from '@/request/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
|
|
@ -12,7 +12,6 @@ import {
|
|||
RadioGroup,
|
||||
styled,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
|
||||
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
||||
|
|
@ -37,7 +36,7 @@ const DocumentComments = ({
|
|||
data: DomainAppDetailResp;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
|
|
@ -57,8 +56,6 @@ const DocumentComments = ({
|
|||
);
|
||||
}, [data]);
|
||||
|
||||
const isPro = license.edition === 1 || license.edition === 2;
|
||||
|
||||
const onSubmit = handleSubmit(formData => {
|
||||
putApiV1App(
|
||||
{ id: data.id! },
|
||||
|
|
@ -108,7 +105,7 @@ const DocumentComments = ({
|
|||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='评论审核' tooltip={!isPro && '联创版和企业版可用'}>
|
||||
<FormItem label='评论审核' permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='moderation_enable'
|
||||
|
|
@ -116,7 +113,6 @@ const DocumentComments = ({
|
|||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
value={isPro ? field.value : undefined}
|
||||
onChange={e => {
|
||||
setIsEdit(true);
|
||||
field.onChange(+e.target.value as 1 | 0);
|
||||
|
|
@ -124,12 +120,12 @@ const DocumentComments = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={1}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>启用</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={0}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
@ -150,7 +146,7 @@ const AIQuestion = ({
|
|||
refresh: () => void;
|
||||
}) => {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const { kb_id, license } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
is_enabled: true,
|
||||
|
|
@ -159,7 +155,6 @@ const AIQuestion = ({
|
|||
},
|
||||
});
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const isEnterprise = license.edition === 2;
|
||||
|
||||
const onSubmit = handleSubmit(formData => {
|
||||
putApiV1App(
|
||||
|
|
@ -273,7 +268,7 @@ const AIQuestion = ({
|
|||
)}
|
||||
/>{' '}
|
||||
</FormItem>
|
||||
<FormItem label='免责声明' tooltip={!isEnterprise && '企业版可用'}>
|
||||
<FormItem label='免责声明' permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='disclaimer'
|
||||
|
|
@ -282,7 +277,6 @@ const AIQuestion = ({
|
|||
{...field}
|
||||
fullWidth
|
||||
value={field.value || ''}
|
||||
disabled={!isEnterprise}
|
||||
placeholder='请输入免责声明'
|
||||
onChange={e => {
|
||||
setIsEdit(true);
|
||||
|
|
@ -304,7 +298,7 @@ const DocumentContribution = ({
|
|||
refresh: () => void;
|
||||
}) => {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
is_enable: false,
|
||||
|
|
@ -330,7 +324,6 @@ const DocumentContribution = ({
|
|||
});
|
||||
});
|
||||
|
||||
const isPro = license.edition === 1 || license.edition === 2;
|
||||
useEffect(() => {
|
||||
setValue(
|
||||
'is_enable',
|
||||
|
|
@ -340,21 +333,8 @@ const DocumentContribution = ({
|
|||
}, [data]);
|
||||
|
||||
return (
|
||||
<SettingCardItem
|
||||
title={
|
||||
<>
|
||||
文档贡献
|
||||
{!isPro && (
|
||||
<Tooltip title='联创版和企业版可用' placement='top' arrow>
|
||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
isEdit={isEdit}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<FormItem label='文档贡献'>
|
||||
<SettingCardItem title='文档贡献' isEdit={isEdit} onSubmit={onSubmit}>
|
||||
<FormItem label='文档贡献' permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='is_enable'
|
||||
|
|
@ -362,7 +342,7 @@ const DocumentContribution = ({
|
|||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
value={isPro ? field.value : undefined}
|
||||
value={field.value}
|
||||
onChange={e => {
|
||||
setIsEdit(true);
|
||||
field.onChange(e.target.value === 'true');
|
||||
|
|
@ -370,12 +350,12 @@ const DocumentContribution = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={true}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>启用</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={false}
|
||||
control={<Radio size='small' disabled={!isPro} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ import { Controller, useForm } from 'react-hook-form';
|
|||
import { useDispatch } from 'react-redux';
|
||||
import AddRole from './AddRole';
|
||||
import { Form, FormItem, SettingCardItem } from './Common';
|
||||
import {
|
||||
PROFESSION_VERSION_PERMISSION,
|
||||
BUSINESS_VERSION_PERMISSION,
|
||||
} from '@/constant/version';
|
||||
|
||||
type ApiTokenPermission =
|
||||
GithubComChaitinPandaWikiProApiTokenV1CreateAPITokenReq['permission'];
|
||||
|
|
@ -69,8 +73,8 @@ const ApiToken = () => {
|
|||
perm: ConstsUserKBPermission.UserKBPermissionFullControl,
|
||||
},
|
||||
});
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
const isBusiness = useMemo(() => {
|
||||
return BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license]);
|
||||
|
||||
const onDeleteApiToken = (id: string, name: string) => {
|
||||
|
|
@ -131,9 +135,9 @@ const ApiToken = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb_id) return;
|
||||
if (!kb_id || !isBusiness) return;
|
||||
getApiTokenList();
|
||||
}, [kb_id]);
|
||||
}, [kb_id, isBusiness]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!addOpen) reset();
|
||||
|
|
@ -142,27 +146,17 @@ const ApiToken = () => {
|
|||
return (
|
||||
<SettingCardItem
|
||||
title='API Token'
|
||||
permission={BUSINESS_VERSION_PERMISSION}
|
||||
extra={
|
||||
<Stack direction={'row'} alignItems={'center'}>
|
||||
<Button
|
||||
color='primary'
|
||||
size='small'
|
||||
disabled={!isEnterprise}
|
||||
onClick={() => setAddOpen(true)}
|
||||
sx={{ textTransform: 'none' }}
|
||||
>
|
||||
创建 API Token
|
||||
</Button>
|
||||
|
||||
<Tooltip title={'企业版可用'} placement='top' arrow>
|
||||
<InfoIcon
|
||||
sx={{
|
||||
color: 'text.secondary',
|
||||
fontSize: 14,
|
||||
display: !isEnterprise ? 'block' : 'none',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
}
|
||||
>
|
||||
|
|
@ -232,7 +226,7 @@ const ApiToken = () => {
|
|||
size='small'
|
||||
sx={{ width: 120 }}
|
||||
value={it.permission}
|
||||
disabled={!isEnterprise || user.role !== 'admin'}
|
||||
disabled={!isBusiness || user.role !== 'admin'}
|
||||
onChange={e =>
|
||||
onUpdateApiToken(it.id!, e.target.value as ApiTokenPermission)
|
||||
}
|
||||
|
|
@ -259,7 +253,7 @@ const ApiToken = () => {
|
|||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? '权限不足'
|
||||
: '企业版可用'
|
||||
: '商业版可用'
|
||||
}
|
||||
placement='top'
|
||||
arrow
|
||||
|
|
@ -270,7 +264,7 @@ const ApiToken = () => {
|
|||
fontSize: 14,
|
||||
ml: 1,
|
||||
visibility:
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? 'visible'
|
||||
|
|
@ -285,13 +279,13 @@ const ApiToken = () => {
|
|||
type='icon-icon_tool_close'
|
||||
sx={{
|
||||
cursor:
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? 'not-allowed'
|
||||
: 'pointer',
|
||||
color:
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
? 'text.disabled'
|
||||
|
|
@ -299,7 +293,7 @@ const ApiToken = () => {
|
|||
}}
|
||||
onClick={() => {
|
||||
if (
|
||||
!isEnterprise ||
|
||||
!isBusiness ||
|
||||
kbDetail?.perm !==
|
||||
ConstsUserKBPermission.UserKBPermissionFullControl
|
||||
)
|
||||
|
|
@ -367,17 +361,16 @@ const ApiToken = () => {
|
|||
>
|
||||
完全控制
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDocManage}
|
||||
>
|
||||
文档管理 {isEnterprise ? '' : '(企业版可用)'}
|
||||
文档管理
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!isEnterprise}
|
||||
value={ConstsUserKBPermission.UserKBPermissionDataOperate}
|
||||
>
|
||||
数据运营 {isEnterprise ? '' : '(企业版可用)'}
|
||||
数据运营
|
||||
</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
|
|
@ -405,9 +398,9 @@ const CardKB = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
const isPro = useMemo(() => {
|
||||
return PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
}, [license.edition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb_id) return;
|
||||
|
|
@ -513,7 +506,7 @@ const CardKB = () => {
|
|||
size='small'
|
||||
sx={{ width: 180 }}
|
||||
value={it.perms}
|
||||
disabled={!isEnterprise || it.role === 'admin'}
|
||||
disabled={!isPro || it.role === 'admin'}
|
||||
onChange={e =>
|
||||
onUpdateUserPermission(
|
||||
it.id!,
|
||||
|
|
@ -542,7 +535,7 @@ const CardKB = () => {
|
|||
title={
|
||||
it.role === 'admin'
|
||||
? '超级管理员不可被修改权限'
|
||||
: '企业版可用'
|
||||
: '专业版可用'
|
||||
}
|
||||
placement='top'
|
||||
arrow
|
||||
|
|
@ -553,9 +546,7 @@ const CardKB = () => {
|
|||
fontSize: 14,
|
||||
ml: 1,
|
||||
visibility:
|
||||
!isEnterprise || it.role === 'admin'
|
||||
? 'visible'
|
||||
: 'hidden',
|
||||
!isPro || it.role === 'admin' ? 'visible' : 'hidden',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@ import {
|
|||
} from '@/request/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { Icon, message } from '@ctzhian/ui';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Collapse,
|
||||
FormControlLabel,
|
||||
Link,
|
||||
Radio,
|
||||
|
|
@ -31,6 +34,8 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
|
||||
const [widgetConfigOpen, setWidgetConfigOpen] = useState(false);
|
||||
const [modalConfigOpen, setModalConfigOpen] = useState(false);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const {
|
||||
control,
|
||||
|
|
@ -43,8 +48,15 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
defaultValues: {
|
||||
is_open: 0,
|
||||
theme_mode: 'light',
|
||||
btn_style: 'hover_ball',
|
||||
btn_id: '',
|
||||
btn_position: 'bottom_right',
|
||||
disclaimer: '',
|
||||
btn_text: '',
|
||||
btn_logo: '',
|
||||
modal_position: 'follow',
|
||||
search_mode: 'all',
|
||||
placeholder: '',
|
||||
recommend_questions: [] as string[],
|
||||
recommend_node_ids: [] as string[],
|
||||
},
|
||||
|
|
@ -54,6 +66,8 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
|
||||
const recommend_questions = watch('recommend_questions') || [];
|
||||
const recommend_node_ids = watch('recommend_node_ids') || [];
|
||||
const btn_style = watch('btn_style') || 'hover_ball';
|
||||
const isCustomButton = btn_style === 'btn_trigger';
|
||||
|
||||
const recommendQuestionsField = useCommitPendingInput<string>({
|
||||
value: recommend_questions,
|
||||
|
|
@ -87,8 +101,17 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
reset({
|
||||
is_open: res.settings?.widget_bot_settings?.is_open ? 1 : 0,
|
||||
theme_mode: res.settings?.widget_bot_settings?.theme_mode || 'light',
|
||||
btn_style: res.settings?.widget_bot_settings?.btn_style || 'hover_ball',
|
||||
btn_id: res.settings?.widget_bot_settings?.btn_id || '',
|
||||
btn_position:
|
||||
res.settings?.widget_bot_settings?.btn_position || 'bottom_right',
|
||||
btn_text: res.settings?.widget_bot_settings?.btn_text || '在线客服',
|
||||
btn_logo: res.settings?.widget_bot_settings?.btn_logo,
|
||||
btn_logo: res.settings?.widget_bot_settings?.btn_logo || '',
|
||||
modal_position:
|
||||
res.settings?.widget_bot_settings?.modal_position || 'follow',
|
||||
search_mode: res.settings?.widget_bot_settings?.search_mode || 'all',
|
||||
placeholder: res.settings?.widget_bot_settings?.placeholder || '',
|
||||
disclaimer: res.settings?.widget_bot_settings?.disclaimer || '',
|
||||
recommend_questions:
|
||||
res.settings?.widget_bot_settings?.recommend_questions || [],
|
||||
recommend_node_ids:
|
||||
|
|
@ -108,8 +131,15 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
widget_bot_settings: {
|
||||
is_open: data.is_open === 1 ? true : false,
|
||||
theme_mode: data.theme_mode as 'light' | 'dark',
|
||||
btn_style: data.btn_style,
|
||||
btn_id: data.btn_id,
|
||||
btn_position: data.btn_position,
|
||||
btn_text: data.btn_text,
|
||||
btn_logo: data.btn_logo,
|
||||
modal_position: data.modal_position,
|
||||
search_mode: data.search_mode,
|
||||
placeholder: data.placeholder,
|
||||
disclaimer: data.disclaimer,
|
||||
recommend_questions: data.recommend_questions || [],
|
||||
recommend_node_ids: data.recommend_node_ids || [],
|
||||
},
|
||||
|
|
@ -151,146 +181,469 @@ const CardRobotWebComponent = ({ kb }: CardRobotWebComponentProps) => {
|
|||
</Link>
|
||||
}
|
||||
>
|
||||
<FormItem label='网页挂件机器人'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='is_open'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(+e.target.value as 1 | 0);
|
||||
setIsEnabled((+e.target.value as 1 | 0) === 1);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
<Stack spacing={3}>
|
||||
<FormItem label='网页挂件机器人'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='is_open'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(+e.target.value as 1 | 0);
|
||||
setIsEnabled((+e.target.value as 1 | 0) === 1);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={1}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>启用</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={0}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>禁用</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
{isEnabled && (
|
||||
<>
|
||||
<FormItem
|
||||
label='嵌入代码'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={1}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>启用</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={0}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>禁用</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
{isEnabled && (
|
||||
<>
|
||||
<FormItem label='配色方案'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='theme_mode'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
{url ? (
|
||||
<ShowText
|
||||
noEllipsis
|
||||
text={[
|
||||
`<!--// Head 标签引入样式 -->`,
|
||||
`<link rel="stylesheet" href="${url}/widget-bot.css">`,
|
||||
`<!--// Body 标签引入挂件 -->`,
|
||||
`<script src="${url}/widget-bot.js"></script>`,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems={'center'}
|
||||
gap={0.5}
|
||||
sx={{
|
||||
color: 'warning.main',
|
||||
fontSize: 14,
|
||||
p: 1.5,
|
||||
borderRadius: 1,
|
||||
bgcolor: 'warning.light',
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={'light'}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>浅色模式</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={'dark'}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>深色模式</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
<Icon type='icon-jinggao' />
|
||||
未配置域名,可在右侧
|
||||
<Box component={'span'} sx={{ fontWeight: 500 }}>
|
||||
服务监听方式
|
||||
</Box>{' '}
|
||||
中配置
|
||||
</Stack>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='侧边按钮文字'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_text'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
{...field}
|
||||
placeholder='输入侧边按钮文字'
|
||||
error={!!errors.btn_text}
|
||||
helperText={errors.btn_text?.message}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='侧边按钮 Logo'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_logo'
|
||||
render={({ field }) => (
|
||||
<UploadFile
|
||||
{...field}
|
||||
id='btn_logo'
|
||||
type='url'
|
||||
accept='image/*'
|
||||
width={80}
|
||||
onChange={url => {
|
||||
field.onChange(url);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='推荐问题'>
|
||||
<FreeSoloAutocomplete
|
||||
{...recommendQuestionsField}
|
||||
placeholder='回车确认,填写下一个推荐问题'
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='推荐文档'>
|
||||
<RecommendDocDragList
|
||||
ids={recommend_node_ids}
|
||||
onChange={(value: string[]) => {
|
||||
setIsEdit(true);
|
||||
setValue('recommend_node_ids', value);
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label='嵌入代码'>
|
||||
{url ? (
|
||||
<ShowText
|
||||
noEllipsis
|
||||
text={[
|
||||
`<!--// Head 标签引入样式 -->`,
|
||||
`<link rel="stylesheet" href="${url}/widget-bot.css">`,
|
||||
'',
|
||||
`<!--// Body 标签引入挂件 -->`,
|
||||
`<script src="${url}/widget-bot.js"></script>`,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Stack
|
||||
direction='row'
|
||||
alignItems={'center'}
|
||||
gap={0.5}
|
||||
sx={{ color: 'warning.main', fontSize: 14 }}
|
||||
>
|
||||
<Icon type='icon-jinggao' />
|
||||
未配置域名,可在右侧
|
||||
<Box component={'span'} sx={{ fontWeight: 500 }}>
|
||||
服务监听方式
|
||||
</Box>{' '}
|
||||
中配置
|
||||
</Stack>
|
||||
)}
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='挂件配置'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Box>
|
||||
{!widgetConfigOpen && (
|
||||
<Button
|
||||
size='small'
|
||||
variant='outlined'
|
||||
onClick={() => setWidgetConfigOpen(true)}
|
||||
endIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
展开
|
||||
</Button>
|
||||
)}
|
||||
<Collapse in={widgetConfigOpen}>
|
||||
<Stack spacing={2.5}>
|
||||
<FormItem
|
||||
label='按钮样式'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_style'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
const value = e.target.value;
|
||||
field.onChange(value);
|
||||
if (value === 'btn_trigger') {
|
||||
setValue('modal_position', 'fixed');
|
||||
}
|
||||
setIsEdit(true);
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value='hover_ball'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>悬浮球</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='side_sticky'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>侧边吸附</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='btn_trigger'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>自定义按钮</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
{isCustomButton ? (
|
||||
<FormItem
|
||||
label='自定义按钮 ID'
|
||||
required
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_id'
|
||||
rules={{
|
||||
required: '自定义按钮 ID 不能为空',
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
placeholder='嵌入网站中自定义按钮的 #id 点击触发,如: pandawiki-widget-bot-btn'
|
||||
error={!!errors.btn_id}
|
||||
helperText={errors.btn_id?.message}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
) : (
|
||||
<>
|
||||
<FormItem
|
||||
label='按钮位置'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_position'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value='top_left'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>左上</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='top_right'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>右上</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='bottom_left'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>左下</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='bottom_right'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>右下</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
{btn_style !== 'hover_ball' && (
|
||||
<FormItem
|
||||
label='按钮文字'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_text'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
fullWidth
|
||||
placeholder='输入按钮文字'
|
||||
error={!!errors.btn_text}
|
||||
helperText={errors.btn_text?.message}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
<FormItem
|
||||
label='按钮图标'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='btn_logo'
|
||||
render={({ field }) => (
|
||||
<UploadFile
|
||||
{...field}
|
||||
id='btn_logo'
|
||||
type='url'
|
||||
accept='image/*'
|
||||
width={80}
|
||||
onChange={url => {
|
||||
field.onChange(url);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='弹框配置'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Box>
|
||||
{!modalConfigOpen && (
|
||||
<Button
|
||||
size='small'
|
||||
variant='outlined'
|
||||
onClick={() => setModalConfigOpen(true)}
|
||||
endIcon={<ExpandMoreIcon />}
|
||||
>
|
||||
展开
|
||||
</Button>
|
||||
)}
|
||||
<Collapse in={modalConfigOpen}>
|
||||
<Stack spacing={2.5}>
|
||||
{/* <FormItem label='配色方案' sx={{ alignItems: 'flex-start' }} labelSx={{ mt: 1 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='theme_mode'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value='light'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>浅色模式</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='dark'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>深色模式</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='system'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>跟随系统</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='wiki'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>跟随 WIKI 网站</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem> */}
|
||||
<FormItem
|
||||
label='弹窗位置'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='modal_position'
|
||||
render={({ field }) => {
|
||||
const isDisabled = btn_style === 'btn_trigger';
|
||||
return (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
value={isDisabled ? 'fixed' : field.value}
|
||||
onChange={e => {
|
||||
if (!isDisabled) {
|
||||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value='follow'
|
||||
control={
|
||||
<Radio size='small' disabled={isDisabled} />
|
||||
}
|
||||
label={<Box sx={{ width: 100 }}>跟随按钮</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='fixed'
|
||||
control={
|
||||
<Radio size='small' disabled={isDisabled} />
|
||||
}
|
||||
label={<Box sx={{ width: 100 }}>居中展示</Box>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='搜索模式'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='search_mode'
|
||||
render={({ field }) => (
|
||||
<RadioGroup
|
||||
row
|
||||
{...field}
|
||||
onChange={e => {
|
||||
field.onChange(e.target.value);
|
||||
setIsEdit(true);
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
value='all'
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>双模式切换</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='qa'
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
<Box sx={{ width: 100 }}>智能问答模式</Box>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value='doc'
|
||||
control={<Radio size='small' />}
|
||||
label={
|
||||
<Box sx={{ width: 100 }}>搜索文档模式</Box>
|
||||
}
|
||||
/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='搜索提示语'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='placeholder'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
{...field}
|
||||
placeholder='输入搜索提示语'
|
||||
error={!!errors.placeholder}
|
||||
helperText={errors.placeholder?.message}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='推荐问题'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
{...recommendQuestionsField}
|
||||
placeholder='回车确认,填写下一个推荐问题'
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='推荐文档'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<RecommendDocDragList
|
||||
ids={recommend_node_ids}
|
||||
onChange={(value: string[]) => {
|
||||
setIsEdit(true);
|
||||
setValue('recommend_node_ids', value);
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label='免责声明'
|
||||
sx={{ alignItems: 'flex-start' }}
|
||||
labelSx={{ mt: 1 }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name='disclaimer'
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
fullWidth
|
||||
{...field}
|
||||
placeholder='输入免责声明'
|
||||
error={!!errors.disclaimer}
|
||||
helperText={errors.disclaimer?.message}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</SettingCardItem>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { DomainKnowledgeBaseDetail } from '@/request/types';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
Link,
|
||||
|
|
@ -13,10 +12,11 @@ import {
|
|||
import ShowText from '@/components/ShowText';
|
||||
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormItem, SettingCardItem } from './Common';
|
||||
import { DomainAppDetailResp } from '@/request/types';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import { useAppSelector } from '@/store';
|
||||
|
||||
const CardRobotApi = ({
|
||||
|
|
@ -29,11 +29,6 @@ const CardRobotApi = ({
|
|||
const [isEdit, setIsEdit] = useState(false);
|
||||
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
|
||||
const { license } = useAppSelector(state => state.config);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
|
|
@ -114,10 +109,7 @@ const CardRobotApi = ({
|
|||
}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<FormItem
|
||||
label='问答机器人 API'
|
||||
tooltip={!isEnterprise ? '企业版可用' : undefined}
|
||||
>
|
||||
<FormItem label='问答机器人 API' permission={BUSINESS_VERSION_PERMISSION}>
|
||||
<FormControl>
|
||||
<Controller
|
||||
control={control}
|
||||
|
|
@ -133,13 +125,11 @@ const CardRobotApi = ({
|
|||
<Stack direction={'row'}>
|
||||
<FormControlLabel
|
||||
value={true}
|
||||
disabled={!isEnterprise}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>启用</Box>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={false}
|
||||
disabled={!isEnterprise}
|
||||
control={<Radio size='small' />}
|
||||
label={<Box sx={{ width: 100 }}>禁用</Box>}
|
||||
/>
|
||||
|
|
@ -150,7 +140,7 @@ const CardRobotApi = ({
|
|||
</FormControl>
|
||||
</FormItem>
|
||||
|
||||
{isEnabled && (
|
||||
{isEnabled && BUSINESS_VERSION_PERMISSION.includes(license.edition!) && (
|
||||
<>
|
||||
<FormItem label='API Token' required>
|
||||
<Controller
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import {
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { FormItem, SettingCardItem } from './Common';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
import { PROFESSION_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
const CardRobotWecomService = ({
|
||||
kb,
|
||||
|
|
@ -262,38 +264,40 @@ const CardRobotWecomService = ({
|
|||
<Icon type='icon-jinggao' sx={{ fontSize: 18 }} />
|
||||
人工客服转接配置:当用户触发以下场景时,会自动转接人工客服
|
||||
</Stack>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
包含特定
|
||||
<VersionMask permission={PROFESSION_VERSION_PERMISSION}>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
包含特定
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...containKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
完全匹配
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...containKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={
|
||||
<Box>
|
||||
提问
|
||||
<Box component={'span'} sx={{ fontWeight: 600 }}>
|
||||
完全匹配
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
关键词
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...equalKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
}
|
||||
>
|
||||
<FreeSoloAutocomplete
|
||||
placeholder='回车确认,填写下一个'
|
||||
{...equalKeywordsField}
|
||||
/>
|
||||
</FormItem>
|
||||
</VersionMask>
|
||||
</>
|
||||
)}
|
||||
</SettingCardItem>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from '@/request/types';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { message } from '@ctzhian/ui';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
|
|
@ -17,7 +18,7 @@ import {
|
|||
TextField,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { FormItem, SettingCardItem } from './Common';
|
||||
|
||||
|
|
@ -32,15 +33,9 @@ const WatermarkForm = ({
|
|||
data?: DomainAppDetailResp;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const [watermarkIsEdit, setWatermarkIsEdit] = useState(false);
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
const { control, handleSubmit, setValue, watch } = useForm({
|
||||
defaultValues: {
|
||||
watermark_setting: data?.settings?.watermark_setting ?? null,
|
||||
watermark_content: data?.settings?.watermark_content ?? '',
|
||||
|
|
@ -48,9 +43,6 @@ const WatermarkForm = ({
|
|||
});
|
||||
|
||||
const watermarkSetting = watch('watermark_setting');
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const handleSaveWatermark = handleSubmit(values => {
|
||||
if (!data?.id || values.watermark_setting === null) return;
|
||||
|
|
@ -82,8 +74,9 @@ const WatermarkForm = ({
|
|||
title='水印'
|
||||
isEdit={watermarkIsEdit}
|
||||
onSubmit={handleSaveWatermark}
|
||||
permission={BUSINESS_VERSION_PERMISSION}
|
||||
>
|
||||
<FormItem label='水印开关' tooltip={!isEnterprise && '企业版可用'}>
|
||||
<FormItem label='水印开关'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='watermark_setting'
|
||||
|
|
@ -98,18 +91,18 @@ const WatermarkForm = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={ConstsWatermarkSetting.WatermarkVisible}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>显性水印</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={ConstsWatermarkSetting.WatermarkHidden}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>隐形水印</StyledRadioLabel>}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
value={ConstsWatermarkSetting.WatermarkDisabled}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁用</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
@ -128,7 +121,6 @@ const WatermarkForm = ({
|
|||
placeholder='请输入水印内容, 支持多行输入'
|
||||
multiline
|
||||
minRows={2}
|
||||
disabled={!isEnterprise}
|
||||
onChange={e => {
|
||||
setWatermarkIsEdit(true);
|
||||
field.onChange(e.target.value);
|
||||
|
|
@ -146,9 +138,6 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
|||
const { license } = useAppSelector(state => state.config);
|
||||
const [questionInputValue, setQuestionInputValue] = useState('');
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
|
|
@ -169,17 +158,18 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
|||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!kb.id || !isEnterprise) return;
|
||||
if (!kb.id || !BUSINESS_VERSION_PERMISSION.includes(license.edition!))
|
||||
return;
|
||||
getApiProV1Block({ kb_id: kb.id! }).then(res => {
|
||||
setValue('block_words', res.words || []);
|
||||
});
|
||||
}, [kb, isEnterprise]);
|
||||
}, [kb, license.edition]);
|
||||
|
||||
return (
|
||||
<SettingCardItem title='内容合规' isEdit={isEdit} onSubmit={onSubmit}>
|
||||
<FormItem
|
||||
vertical
|
||||
tooltip={!isEnterprise && '企业版可用'}
|
||||
permission={BUSINESS_VERSION_PERMISSION}
|
||||
label='屏蔽 AI 问答中的关键字'
|
||||
>
|
||||
<Controller
|
||||
|
|
@ -193,7 +183,6 @@ const KeywordsForm = ({ kb }: { kb: DomainKnowledgeBaseDetail }) => {
|
|||
inputValue={questionInputValue}
|
||||
options={[]}
|
||||
fullWidth
|
||||
disabled={!isEnterprise}
|
||||
onInputChange={(_, value) => {
|
||||
setQuestionInputValue(value);
|
||||
}}
|
||||
|
|
@ -234,23 +223,14 @@ const CopyForm = ({
|
|||
data?: DomainAppDetailResp;
|
||||
refresh: () => void;
|
||||
}) => {
|
||||
const { license, kb_id } = useAppSelector(state => state.config);
|
||||
const { kb_id } = useAppSelector(state => state.config);
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
const { control, handleSubmit, setValue } = useForm({
|
||||
defaultValues: {
|
||||
copy_setting: data?.settings?.copy_setting ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const handleSaveWatermark = handleSubmit(values => {
|
||||
if (!data?.id || values.copy_setting === null) return;
|
||||
putApiV1App(
|
||||
|
|
@ -280,7 +260,7 @@ const CopyForm = ({
|
|||
isEdit={isEdit}
|
||||
onSubmit={handleSaveWatermark}
|
||||
>
|
||||
<FormItem label='限制复制' tooltip={!isEnterprise && '企业版可用'}>
|
||||
<FormItem label='限制复制' permission={BUSINESS_VERSION_PERMISSION}>
|
||||
<Controller
|
||||
control={control}
|
||||
name='copy_setting'
|
||||
|
|
@ -295,18 +275,18 @@ const CopyForm = ({
|
|||
>
|
||||
<FormControlLabel
|
||||
value={ConstsCopySetting.CopySettingNone}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>不做限制</StyledRadioLabel>}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={ConstsCopySetting.CopySettingAppend}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>增加内容尾巴</StyledRadioLabel>}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
value={ConstsCopySetting.CopySettingDisabled}
|
||||
control={<Radio size='small' disabled={!isEnterprise} />}
|
||||
control={<Radio size='small' />}
|
||||
label={<StyledRadioLabel>禁止复制内容</StyledRadioLabel>}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
|||
defaultValues: {
|
||||
desc: '',
|
||||
keyword: '',
|
||||
auto_sitemap: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -44,7 +43,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
|||
useEffect(() => {
|
||||
setValue('desc', data.settings?.desc || '');
|
||||
setValue('keyword', data.settings?.keyword || '');
|
||||
setValue('auto_sitemap', data.settings?.auto_sitemap ?? false);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
|
|
@ -88,25 +86,6 @@ const CardWebSEO = ({ data, id, refresh }: CardWebSEOProps) => {
|
|||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label='自动生成 Sitemap'>
|
||||
<Controller
|
||||
control={control}
|
||||
name='auto_sitemap'
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
checked={field.value}
|
||||
size='small'
|
||||
sx={{ p: 0, m: 0 }}
|
||||
onChange={event => {
|
||||
setIsEdit(true);
|
||||
field.onChange(event);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormItem>
|
||||
</SettingCardItem>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import Card from '@/components/Card';
|
||||
import { ConstsLicenseEdition } from '@/request/types';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { Button, Stack, styled, SxProps, Tooltip } from '@mui/material';
|
||||
import { createContext, useContext } from 'react';
|
||||
import VersionMask from '@/components/VersionMask';
|
||||
|
||||
const StyledForm = styled('form')<{ gap?: number | string }>(
|
||||
({ theme, gap = 2 }) => ({
|
||||
|
|
@ -40,6 +42,7 @@ const StyledFormLabel = styled('span')<{ required?: boolean }>(
|
|||
|
||||
export const StyledFormItem = styled('div')<{ vertical?: boolean }>(
|
||||
({ theme, vertical }) => ({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: vertical ? 'flex-start' : 'center',
|
||||
flexDirection: vertical ? 'column' : 'row',
|
||||
|
|
@ -82,6 +85,7 @@ export const FormItem = ({
|
|||
extra,
|
||||
sx,
|
||||
labelSx,
|
||||
permission,
|
||||
}: {
|
||||
label?: string | React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
|
|
@ -92,31 +96,37 @@ export const FormItem = ({
|
|||
extra?: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
labelSx?: SxProps;
|
||||
permission?: number[];
|
||||
}) => {
|
||||
const { vertical: verticalContext, labelWidth: labelWidthContext } =
|
||||
useContext(FormContext);
|
||||
return (
|
||||
<StyledFormItem vertical={vertical || verticalContext} sx={sx}>
|
||||
<StyledFormLabelWrapper
|
||||
vertical={vertical || verticalContext}
|
||||
labelWidth={labelWidth || labelWidthContext}
|
||||
sx={labelSx}
|
||||
>
|
||||
<Stack direction='row' alignItems='center' flex={1}>
|
||||
<StyledFormLabel required={required}>{label}</StyledFormLabel>
|
||||
{tooltip && typeof tooltip === 'string' ? (
|
||||
<Tooltip title={tooltip} placement='top' arrow>
|
||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
tooltip
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{extra}
|
||||
</StyledFormLabelWrapper>
|
||||
{children}
|
||||
</StyledFormItem>
|
||||
return (
|
||||
<VersionMask permission={permission}>
|
||||
<StyledFormItem vertical={vertical || verticalContext} sx={sx}>
|
||||
<StyledFormLabelWrapper
|
||||
vertical={vertical || verticalContext}
|
||||
labelWidth={labelWidth || labelWidthContext}
|
||||
sx={labelSx}
|
||||
>
|
||||
<Stack direction='row' alignItems='center' flex={1}>
|
||||
<StyledFormLabel required={required}>{label}</StyledFormLabel>
|
||||
{tooltip && typeof tooltip === 'string' ? (
|
||||
<Tooltip title={tooltip} placement='top' arrow>
|
||||
<InfoIcon
|
||||
sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
tooltip
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{extra}
|
||||
</StyledFormLabelWrapper>
|
||||
{children}
|
||||
</StyledFormItem>
|
||||
</VersionMask>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -142,6 +152,7 @@ export const SettingCard = ({
|
|||
};
|
||||
|
||||
const StyledSettingCardItem = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
'&:not(:last-child)': {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
paddingBottom: theme.spacing(4),
|
||||
|
|
@ -204,6 +215,12 @@ export const SettingCardItem = ({
|
|||
extra,
|
||||
more,
|
||||
sx,
|
||||
permission = [
|
||||
ConstsLicenseEdition.LicenseEditionFree,
|
||||
ConstsLicenseEdition.LicenseEditionProfession,
|
||||
ConstsLicenseEdition.LicenseEditionBusiness,
|
||||
ConstsLicenseEdition.LicenseEditionEnterprise,
|
||||
],
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
|
|
@ -212,6 +229,7 @@ export const SettingCardItem = ({
|
|||
extra?: React.ReactNode;
|
||||
more?: SettingCardItemMore;
|
||||
sx?: SxProps;
|
||||
permission?: number[];
|
||||
}) => {
|
||||
const renderMore = (more: SettingCardItemMore) => {
|
||||
if (more && typeof more === 'object' && 'type' in more) {
|
||||
|
|
@ -237,20 +255,23 @@ export const SettingCardItem = ({
|
|||
return more;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledSettingCardItem sx={sx}>
|
||||
<StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemTitle>
|
||||
{title} {renderMore(more)}
|
||||
</StyledSettingCardItemTitle>
|
||||
{isEdit && (
|
||||
<Button variant='contained' size='small' onClick={onSubmit}>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
{extra}
|
||||
</StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemContent>{children}</StyledSettingCardItemContent>
|
||||
</StyledSettingCardItem>
|
||||
<VersionMask permission={permission}>
|
||||
<StyledSettingCardItem sx={sx}>
|
||||
<StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemTitle>
|
||||
{title} {renderMore(more)}
|
||||
</StyledSettingCardItemTitle>
|
||||
{isEdit && (
|
||||
<Button variant='contained' size='small' onClick={onSubmit}>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
{extra}
|
||||
</StyledSettingCardItemTitleWrapper>
|
||||
<StyledSettingCardItemContent>{children}</StyledSettingCardItemContent>
|
||||
</StyledSettingCardItem>
|
||||
</VersionMask>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SettingCardItem } from '../Common';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import { Modal, message } from '@ctzhian/ui';
|
||||
import { Stack, Button } from '@mui/material';
|
||||
import { Box } from '@mui/material';
|
||||
import { postApiProV1AuthGroupSync } from '@/request/pro/AuthOrg';
|
||||
import {
|
||||
|
|
@ -19,6 +16,7 @@ import {
|
|||
deleteApiProV1AuthGroupDelete,
|
||||
} from '@/request/pro/AuthGroup';
|
||||
import GroupTree from './GroupTree';
|
||||
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
|
||||
|
||||
interface UserGroupProps {
|
||||
enabled: string;
|
||||
|
|
@ -45,10 +43,6 @@ const UserGroup = ({
|
|||
GithubComChaitinPandaWikiProApiAuthV1AuthGroupTreeItem[]
|
||||
>([]);
|
||||
|
||||
const isEnterprise = useMemo(() => {
|
||||
return license.edition === 2;
|
||||
}, [license]);
|
||||
|
||||
const onDeleteUserGroup = (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '删除用户组',
|
||||
|
|
@ -74,10 +68,15 @@ const UserGroup = ({
|
|||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!kb_id || enabled !== '2' || !isEnterprise) return;
|
||||
if (
|
||||
!kb_id ||
|
||||
enabled !== '2' ||
|
||||
!BUSINESS_VERSION_PERMISSION.includes(license.edition!)
|
||||
)
|
||||
return;
|
||||
getUserGroup();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [kb_id, enabled, isEnterprise]);
|
||||
}, [kb_id, enabled, license.edition!]);
|
||||
|
||||
const handleMove = async ({
|
||||
id,
|
||||
|
|
@ -123,32 +122,7 @@ const UserGroup = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<SettingCardItem
|
||||
title='用户组'
|
||||
more={
|
||||
!isEnterprise && (
|
||||
<Tooltip title='企业版可用' placement='top' arrow>
|
||||
<InfoIcon sx={{ color: 'text.secondary', fontSize: 14, ml: 1 }} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
// extra={
|
||||
// isEnterprise &&
|
||||
// [
|
||||
// ConstsSourceType.SourceTypeWeCom,
|
||||
// ConstsSourceType.SourceTypeDingTalk,
|
||||
// ].includes(sourceType as ConstsSourceType) && (
|
||||
// <Button
|
||||
// color='primary'
|
||||
// size='small'
|
||||
// onClick={handleSync}
|
||||
// loading={syncLoading}
|
||||
// >
|
||||
// 同步组织架构和成员
|
||||
// </Button>
|
||||
// )
|
||||
// }
|
||||
>
|
||||
<SettingCardItem title='用户组' permission={BUSINESS_VERSION_PERMISSION}>
|
||||
<Box
|
||||
sx={{
|
||||
border: '1px dashed',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ import QAReferer from './QAReferer';
|
|||
import RTVisitor from './RTVisitor';
|
||||
import TypeCount from './TypeCount';
|
||||
import { useAppSelector } from '@/store';
|
||||
import { VersionCanUse } from '@/components/VersionMask';
|
||||
import {
|
||||
BUSINESS_VERSION_PERMISSION,
|
||||
PROFESSION_VERSION_PERMISSION,
|
||||
} from '@/constant/version';
|
||||
|
||||
export const TimeList = [
|
||||
{ label: '近 24 小时', value: 1 },
|
||||
|
|
@ -25,13 +30,40 @@ const Statistic = () => {
|
|||
const isWideScreen = useMediaQuery('(min-width:1190px)');
|
||||
|
||||
const timeList = useMemo(() => {
|
||||
const isPro = license.edition === 1 || license.edition === 2;
|
||||
const isEnterprise = license.edition === 2;
|
||||
const isPro = PROFESSION_VERSION_PERMISSION.includes(license.edition!);
|
||||
const isBusiness = BUSINESS_VERSION_PERMISSION.includes(license.edition!);
|
||||
return [
|
||||
{ label: '近 24 小时', value: 1, disabled: false },
|
||||
{ label: '近 7 天', value: 7, disabled: !isPro },
|
||||
{ label: '近 30 天', value: 30, disabled: !isEnterprise },
|
||||
{ label: '近 90 天', value: 90, disabled: !isEnterprise },
|
||||
{
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||
<span>近 7 天</span>
|
||||
<VersionCanUse permission={PROFESSION_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: 7,
|
||||
disabled: !isPro,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||
<span>近 30 天</span>
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: 30,
|
||||
disabled: !isBusiness,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Stack direction={'row'} alignItems={'center'} gap={0.5}>
|
||||
<span>近 90 天</span>
|
||||
<VersionCanUse permission={BUSINESS_VERSION_PERMISSION} />
|
||||
</Stack>
|
||||
),
|
||||
value: 90,
|
||||
disabled: !isBusiness,
|
||||
},
|
||||
];
|
||||
}, [license]);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import {
|
|||
GetApiV1NodeListParams,
|
||||
GetApiV1NodeRecommendNodesParams,
|
||||
V1NodeDetailResp,
|
||||
V1NodeRestudyReq,
|
||||
V1NodeRestudyResp,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
|
|
@ -263,6 +265,38 @@ export const getApiV1NodeRecommendNodes = (
|
|||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 文档重新学习
|
||||
*
|
||||
* @tags Node
|
||||
* @name PostApiV1NodeRestudy
|
||||
* @summary 文档重新学习
|
||||
* @request POST:/api/v1/node/restudy
|
||||
* @secure
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: V1NodeRestudyResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postApiV1NodeRestudy = (
|
||||
param: V1NodeRestudyReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: V1NodeRestudyResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/node/restudy`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Summary Node
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import httpRequest, { ContentType, RequestParams } from "./httpClient";
|
||||
import { DomainResponse, V1NodeRestudyReq, V1NodeRestudyResp } from "./types";
|
||||
|
||||
/**
|
||||
* @description 文档重新学习
|
||||
*
|
||||
* @tags NodeRestudy
|
||||
* @name PostApiV1NodeRestudy
|
||||
* @summary 文档重新学习
|
||||
* @request POST:/api/v1/node/restudy
|
||||
* @secure
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: V1NodeRestudyResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postApiV1NodeRestudy = (
|
||||
param: V1NodeRestudyReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: V1NodeRestudyResp;
|
||||
}
|
||||
>({
|
||||
path: `/api/v1/node/restudy`,
|
||||
method: "POST",
|
||||
body: param,
|
||||
secure: true,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
|
@ -10,7 +10,6 @@ export * from './Message'
|
|||
export * from './Model'
|
||||
export * from './Node'
|
||||
export * from './NodePermission'
|
||||
export * from './NodeRestudy'
|
||||
export * from './Stat'
|
||||
export * from './User'
|
||||
export * from './types'
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
||||
|
|
@ -206,6 +207,32 @@ export const postShareProV1AuthLdap = (
|
|||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 用户登出
|
||||
*
|
||||
* @tags ShareAuth
|
||||
* @name PostShareProV1AuthLogout
|
||||
* @summary 用户登出
|
||||
* @request POST:/share/pro/v1/auth/logout
|
||||
* @response `200` `(DomainPWResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postShareProV1AuthLogout = (params: RequestParams = {}) =>
|
||||
httpRequest<
|
||||
DomainPWResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp;
|
||||
}
|
||||
>({
|
||||
path: `/share/pro/v1/auth/logout`,
|
||||
method: "POST",
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description OAuth登录
|
||||
*
|
||||
|
|
|
|||
|
|
@ -52,10 +52,12 @@ export enum ConstsSourceType {
|
|||
export enum ConstsLicenseEdition {
|
||||
/** 开源版 */
|
||||
LicenseEditionFree = 0,
|
||||
/** 联创版 */
|
||||
LicenseEditionContributor = 1,
|
||||
/** 专业版 */
|
||||
LicenseEditionProfession = 1,
|
||||
/** 企业版 */
|
||||
LicenseEditionEnterprise = 2,
|
||||
/** 商业版 */
|
||||
LicenseEditionBusiness = 3,
|
||||
}
|
||||
|
||||
export enum ConstsContributeType {
|
||||
|
|
@ -455,6 +457,11 @@ export type GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp = Record<
|
|||
any
|
||||
>;
|
||||
|
||||
export type GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp = Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
||||
kb_id?: string;
|
||||
redirect_url?: string;
|
||||
|
|
@ -465,6 +472,7 @@ export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp {
|
|||
}
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthWecomReq {
|
||||
is_app?: boolean;
|
||||
kb_id?: string;
|
||||
redirect_url?: string;
|
||||
}
|
||||
|
|
@ -668,8 +676,6 @@ export interface GetApiProV1TokenListParams {
|
|||
}
|
||||
|
||||
export interface PostApiV1LicensePayload {
|
||||
/** license edition */
|
||||
license_edition: "contributor" | "enterprise";
|
||||
/** license type */
|
||||
license_type: "file" | "code";
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -171,14 +171,21 @@ export enum ConstsNodeAccessPerm {
|
|||
NodeAccessPermClosed = "closed",
|
||||
}
|
||||
|
||||
export enum ConstsModelSettingMode {
|
||||
ModelSettingModeManual = "manual",
|
||||
ModelSettingModeAuto = "auto",
|
||||
}
|
||||
|
||||
/** @format int32 */
|
||||
export enum ConstsLicenseEdition {
|
||||
/** 开源版 */
|
||||
LicenseEditionFree = 0,
|
||||
/** 联创版 */
|
||||
LicenseEditionContributor = 1,
|
||||
/** 专业版 */
|
||||
LicenseEditionProfession = 1,
|
||||
/** 企业版 */
|
||||
LicenseEditionEnterprise = 2,
|
||||
/** 商业版 */
|
||||
LicenseEditionBusiness = 3,
|
||||
}
|
||||
|
||||
export enum ConstsHomePageSetting {
|
||||
|
|
@ -927,8 +934,10 @@ export interface DomainModelModeSetting {
|
|||
auto_mode_api_key?: string;
|
||||
/** 自定义对话模型名称 */
|
||||
chat_model?: string;
|
||||
/** 手动模式下嵌入模型是否更新 */
|
||||
is_manual_embedding_updated?: boolean;
|
||||
/** 模式: manual 或 auto */
|
||||
mode?: string;
|
||||
mode?: ConstsModelSettingMode;
|
||||
}
|
||||
|
||||
export interface DomainMoveNodeReq {
|
||||
|
|
@ -1180,6 +1189,17 @@ export interface DomainShareConversationMessage {
|
|||
role?: SchemaRoleType;
|
||||
}
|
||||
|
||||
export interface DomainShareNodeListItemResp {
|
||||
emoji?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
parent_id?: string;
|
||||
permissions?: DomainNodePermissions;
|
||||
position?: number;
|
||||
type?: DomainNodeType;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface DomainSimpleAuth {
|
||||
enabled?: boolean;
|
||||
password?: string;
|
||||
|
|
@ -1357,11 +1377,18 @@ export interface DomainWecomAIBotSettings {
|
|||
}
|
||||
|
||||
export interface DomainWidgetBotSettings {
|
||||
btn_id?: string;
|
||||
btn_logo?: string;
|
||||
btn_position?: string;
|
||||
btn_style?: string;
|
||||
btn_text?: string;
|
||||
disclaimer?: string;
|
||||
is_open?: boolean;
|
||||
modal_position?: string;
|
||||
placeholder?: string;
|
||||
recommend_node_ids?: string[];
|
||||
recommend_questions?: string[];
|
||||
search_mode?: string;
|
||||
theme_mode?: string;
|
||||
}
|
||||
|
||||
|
|
@ -1645,8 +1672,9 @@ export interface V1NodePermissionResp {
|
|||
}
|
||||
|
||||
export interface V1NodeRestudyReq {
|
||||
kb_id?: string;
|
||||
node_ids?: string[];
|
||||
kb_id: string;
|
||||
/** @minItems 1 */
|
||||
node_ids: string[];
|
||||
}
|
||||
|
||||
export type V1NodeRestudyResp = Record<string, any>;
|
||||
|
|
@ -1666,6 +1694,7 @@ export interface V1ShareNodeDetailResp {
|
|||
editor_id?: string;
|
||||
id?: string;
|
||||
kb_id?: string;
|
||||
list?: DomainShareNodeListItemResp[];
|
||||
meta?: DomainNodeMeta;
|
||||
name?: string;
|
||||
parent_id?: string;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:20-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export default isDevelopment
|
|||
|
||||
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||
// This can increase your server load as well as your hosting bill.
|
||||
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||
// Note: Check that the configured route will not match with your Next.js proxy, otherwise reporting of client-
|
||||
// side errors will fail.
|
||||
tunnelRoute: '/monitoring',
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"version": "2.9.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 3010",
|
||||
"dev": "next dev -p 3010",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@mui/material-nextjs": "^7.1.0",
|
||||
"@mui/material-nextjs": "^7.3.5",
|
||||
"@sentry/nextjs": "^10.8.0",
|
||||
"@types/markdown-it": "13.0.1",
|
||||
"@vscode/markdown-it-katex": "^1.1.2",
|
||||
|
|
@ -25,12 +25,13 @@
|
|||
"highlight.js": "^11.11.1",
|
||||
"html-react-parser": "^5.2.5",
|
||||
"html-to-image": "^1.11.13",
|
||||
"import-in-the-middle": "^1.4.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"katex": "^0.16.22",
|
||||
"markdown-it": "13.0.1",
|
||||
"markdown-it-highlightjs": "^4.2.0",
|
||||
"mermaid": "^11.9.0",
|
||||
"next": "15.4.6",
|
||||
"next": "^16.0.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-photo-view": "^1.2.7",
|
||||
|
|
@ -41,17 +42,23 @@
|
|||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"require-in-the-middle": "^7.5.2",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ctzhian/cx-swagger-api": "^1.0.0",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@next/eslint-plugin-next": "^15.4.5",
|
||||
"@next/eslint-plugin-next": "^16.0.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/rangy": "^1.3.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"eslint-config-next": "15.3.2",
|
||||
"eslint-config-next": "16.0.0",
|
||||
"eslint-config-prettier": "^9.1.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"
|
||||
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"require-in-the-middle": "^7.5.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,39 @@
|
|||
/* 挂件按钮样式 - 基于MUI主题 */
|
||||
/* 挂件按钮基础样式 */
|
||||
.widget-bot-button {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 190px;
|
||||
z-index: 9999;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border-radius: 18px 0 0 18px;
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0px 6px 10px 0px rgba(54, 59, 76, 0.17);
|
||||
padding: 11px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
|
||||
border: none;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
/* 优化拖拽性能 */
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.widget-bot-button:hover {
|
||||
.widget-bot-button:hover:not(.dragging) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(50, 72, 242, 0.2);
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball:hover:not(.dragging) {
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
||||
.widget-bot-button.dragging {
|
||||
cursor: grabbing;
|
||||
transform: rotate(2deg);
|
||||
box-shadow: 0 6px 12px rgba(50, 72, 242, 0.25);
|
||||
transition: none !important;
|
||||
/* 拖拽时禁用过渡,提升性能 */
|
||||
/* transform 由 JS 控制,包含 rotate 和 translate */
|
||||
}
|
||||
|
||||
.widget-bot-button-content {
|
||||
|
|
@ -39,14 +43,13 @@
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
.widget-bot-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-bottom: 8px;
|
||||
/* 图标样式 */
|
||||
.widget-bot-icon {
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* 文字样式 */
|
||||
.widget-bot-text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
|
@ -60,6 +63,47 @@
|
|||
margin: 1px 0;
|
||||
}
|
||||
|
||||
/* 侧边吸附按钮样式 */
|
||||
.widget-bot-side-sticky {
|
||||
width: 48px;
|
||||
padding: 6px 6px 12px 6px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
||||
border-radius: 24px;
|
||||
border: 1px solid #ECEEF1;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.widget-bot-side-sticky .widget-bot-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.widget-bot-side-sticky .widget-bot-text {
|
||||
font-size: 12px;
|
||||
color: #646a73;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
/* 悬浮球按钮样式 */
|
||||
.widget-bot-hover-ball {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
||||
border: 1px solid #ECEEF1;
|
||||
padding: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball .widget-bot-hover-ball-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 模态框样式 - 基于MUI主题 */
|
||||
.widget-bot-modal {
|
||||
position: fixed;
|
||||
|
|
@ -75,6 +119,11 @@
|
|||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.widget-bot-modal-fixed {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.widget-bot-modal-content {
|
||||
position: absolute;
|
||||
width: 600px;
|
||||
|
|
@ -88,6 +137,14 @@
|
|||
animation: slideInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.widget-bot-modal-content-fixed {
|
||||
position: relative;
|
||||
width: 800px;
|
||||
height: auto;
|
||||
max-height: 90vh;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
|
@ -100,34 +157,30 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* 关闭按钮样式 - 基于MUI IconButton */
|
||||
/* 关闭按钮样式 - 透明框 */
|
||||
.widget-bot-close-btn {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: none;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
top: 22.5px;
|
||||
right: 16px;
|
||||
background: transparent;
|
||||
width: 36.26px;
|
||||
height: 25px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
border-radius: 0;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
opacity: 0.5;
|
||||
z-index: 10001;
|
||||
transition: all 0.1s ease-in-out;
|
||||
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||
}
|
||||
|
||||
.widget-bot-close-btn:hover {
|
||||
font-size: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.widget-bot-close-btn:active {
|
||||
transform: scale(0.95);
|
||||
z-index: 10001;
|
||||
transition: none;
|
||||
font-family: var(--font-gilory, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
/* 允许鼠标穿透到下方 */
|
||||
}
|
||||
|
||||
/* iframe样式 */
|
||||
|
|
@ -140,6 +193,11 @@
|
|||
background: #F8F9FA;
|
||||
}
|
||||
|
||||
.widget-bot-modal-content-fixed .widget-bot-iframe {
|
||||
min-height: 600px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* 防止页面滚动 */
|
||||
body.widget-bot-modal-open {
|
||||
overflow: hidden;
|
||||
|
|
@ -147,19 +205,34 @@ body.widget-bot-modal-open {
|
|||
|
||||
/* 暗色主题支持 - 基于data-theme属性 */
|
||||
.widget-bot-button[data-theme="dark"] {
|
||||
background: #6E73FE;
|
||||
box-shadow: 0 2px 4px rgba(110, 115, 254, 0.15);
|
||||
}
|
||||
|
||||
.widget-bot-button[data-theme="dark"]:hover {
|
||||
.widget-bot-side-sticky[data-theme="dark"] {
|
||||
background: #6E73FE;
|
||||
}
|
||||
|
||||
.widget-bot-side-sticky[data-theme="dark"]:hover {
|
||||
background: #5d68fd;
|
||||
box-shadow: 0 4px 8px rgba(110, 115, 254, 0.2);
|
||||
}
|
||||
|
||||
.widget-bot-button[data-theme="dark"].dragging {
|
||||
.widget-bot-side-sticky[data-theme="dark"].dragging {
|
||||
box-shadow: 0 6px 12px rgba(110, 115, 254, 0.25);
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="dark"] {
|
||||
background: #6E73FE;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="dark"]:hover {
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="dark"].dragging {
|
||||
box-shadow: 0 8px 20px rgba(110, 115, 254, 0.3);
|
||||
}
|
||||
|
||||
.widget-bot-modal[data-theme="dark"] {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
|
@ -169,61 +242,63 @@ body.widget-bot-modal-open {
|
|||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
/* 移动端适配 - 统一处理 */
|
||||
@media (max-width: 768px) {
|
||||
.widget-bot-button {
|
||||
bottom: 16px;
|
||||
padding: 8px;
|
||||
border-radius: 10px 0 0 10px;
|
||||
.widget-bot-side-sticky {
|
||||
width: 48px;
|
||||
padding: 6px 6px 12px 6px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 24px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball .widget-bot-hover-ball-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.widget-bot-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.widget-bot-logo {
|
||||
.widget-bot-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* 移动端弹框统一居中显示,宽度100%-32px,高度90vh */
|
||||
.widget-bot-modal-content {
|
||||
width: calc(100% - 60.5px);
|
||||
height: 90%;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
position: relative !important;
|
||||
width: calc(100% - 32px) !important;
|
||||
height: 90vh !important;
|
||||
max-width: none !important;
|
||||
max-height: 90vh !important;
|
||||
top: auto !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
margin: auto !important;
|
||||
}
|
||||
|
||||
.widget-bot-modal-content-fixed {
|
||||
width: calc(100% - 32px) !important;
|
||||
height: 90vh !important;
|
||||
max-height: 90vh !important;
|
||||
}
|
||||
|
||||
.widget-bot-close-btn {
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏幕适配 */
|
||||
@media (max-width: 480px) {
|
||||
.widget-bot-button {
|
||||
bottom: 12px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.widget-bot-text {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.widget-bot-modal-content {
|
||||
width: calc(100% - 55.5px);
|
||||
height: 90%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.widget-bot-close-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 14px;
|
||||
top: 22.5px;
|
||||
right: 16px;
|
||||
width: 36.26px;
|
||||
height: 25px;
|
||||
font-size: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -274,19 +349,32 @@ body.widget-bot-modal-open {
|
|||
}
|
||||
|
||||
/* 浅色主题样式 - 显式定义 */
|
||||
.widget-bot-button[data-theme="light"] {
|
||||
background: #3248F2;
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0px 6px 10px 0px rgba(54, 59, 76, 0.17);
|
||||
.widget-bot-side-sticky[data-theme="light"] {
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
||||
border: 1px solid #ECEEF1;
|
||||
}
|
||||
|
||||
.widget-bot-button[data-theme="light"]:hover {
|
||||
background: #2a3cdb;
|
||||
box-shadow: 0 4px 8px rgba(50, 72, 242, 0.2);
|
||||
.widget-bot-side-sticky[data-theme="light"]:hover {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.widget-bot-button[data-theme="light"].dragging {
|
||||
box-shadow: 0 6px 12px rgba(50, 72, 242, 0.25);
|
||||
.widget-bot-side-sticky[data-theme="light"].dragging {
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="light"] {
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 6px 16px 0px rgba(33, 34, 45, 0.02), 0px 8px 40px 0px rgba(50, 73, 45, 0.12);
|
||||
border: 1px solid #ECEEF1;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="light"]:hover {
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
||||
.widget-bot-hover-ball[data-theme="light"].dragging {
|
||||
box-shadow: 0 8px 20px rgba(50, 72, 242, 0.3);
|
||||
}
|
||||
|
||||
.widget-bot-modal[data-theme="light"] {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
const defaultModalPosition = 'follow';
|
||||
const defaultBtnPosition = 'bottom_left';
|
||||
const defaultBtnStyle = 'side_sticky';
|
||||
|
||||
// 获取当前脚本的域名
|
||||
const currentScript = document.currentScript || document.querySelector('script[src*="widget-bot.js"]');
|
||||
const widgetDomain = currentScript ? new URL(currentScript.src).origin : window.location.origin;
|
||||
|
|
@ -11,6 +15,13 @@
|
|||
let isDragging = false;
|
||||
let dragOffset = { x: 0, y: 0 };
|
||||
let currentTheme = 'light'; // 默认浅色主题
|
||||
let customTriggerElement = null; // 自定义触发元素
|
||||
let customTriggerHandler = null; // 自定义触发元素的事件处理函数
|
||||
let dragAnimationFrame = null; // 拖拽动画帧ID
|
||||
let buttonSize = { width: 0, height: 0 }; // 缓存按钮尺寸
|
||||
let initialPosition = { left: 0, top: 0 }; // 拖拽开始时的初始位置
|
||||
let hasDragged = false; // 标记是否发生了拖拽
|
||||
let dragStartPos = { x: 0, y: 0 }; // 拖拽开始时的鼠标位置
|
||||
|
||||
// 应用主题
|
||||
function applyTheme(theme_mode) {
|
||||
|
|
@ -60,13 +71,22 @@
|
|||
applyTheme(widgetInfo.theme_mode);
|
||||
}
|
||||
|
||||
createWidget();
|
||||
// 根据 btn_style 创建不同的挂件
|
||||
const btnStyle = widgetInfo.btn_style || defaultBtnStyle;
|
||||
if (btnStyle === 'btn_trigger') {
|
||||
createCustomTrigger();
|
||||
} else {
|
||||
createWidget();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取挂件信息失败:', error);
|
||||
// 使用默认值
|
||||
widgetInfo = {
|
||||
btn_text: '在线客服',
|
||||
btn_logo: '',
|
||||
btn_logo: `''`,
|
||||
btn_style: defaultBtnStyle,
|
||||
btn_position: defaultBtnPosition,
|
||||
modal_position: defaultModalPosition,
|
||||
theme_mode: 'light'
|
||||
};
|
||||
applyTheme(widgetInfo.theme_mode);
|
||||
|
|
@ -78,53 +98,92 @@
|
|||
}
|
||||
}
|
||||
|
||||
// 创建垂直文字
|
||||
function createVerticalText(text) {
|
||||
return text.split('').map((char, index) =>
|
||||
`<span>${char}</span>`
|
||||
).join('');
|
||||
// 创建两行文字(每行两个字)
|
||||
function createTwoLineText(text) {
|
||||
const chars = text.split('').filter(it => !!it.trim());
|
||||
const lines = [];
|
||||
for (let i = 0; i < chars.length; i += 2) {
|
||||
lines.push(chars.slice(i, i + 2).join(''));
|
||||
}
|
||||
return lines.map(line => `<span>${line}</span>`).join('');
|
||||
}
|
||||
|
||||
// 创建挂件按钮
|
||||
function createWidget() {
|
||||
// 如果已存在,先删除
|
||||
if (widgetButton) {
|
||||
widgetButton.remove();
|
||||
}
|
||||
// 应用按钮位置
|
||||
function applyButtonPosition(button, position) {
|
||||
const pos = position || defaultBtnPosition;
|
||||
button.style.top = 'auto';
|
||||
button.style.right = 'auto';
|
||||
button.style.bottom = 'auto';
|
||||
button.style.left = 'auto';
|
||||
|
||||
// 创建按钮容器
|
||||
// 两种模式使用相同的默认位置:距离边缘16px,垂直方向190px
|
||||
switch (pos) {
|
||||
case 'top_left':
|
||||
button.style.top = '190px';
|
||||
button.style.left = '16px';
|
||||
break;
|
||||
case 'top_right':
|
||||
button.style.top = '190px';
|
||||
button.style.right = '16px';
|
||||
break;
|
||||
case 'bottom_left':
|
||||
button.style.bottom = '190px';
|
||||
button.style.left = '16px';
|
||||
break;
|
||||
case 'bottom_right':
|
||||
default:
|
||||
button.style.bottom = '190px';
|
||||
button.style.right = '16px';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建侧边吸附按钮
|
||||
function createSideStickyButton() {
|
||||
widgetButton = document.createElement('div');
|
||||
widgetButton.className = 'widget-bot-button';
|
||||
widgetButton.className = 'widget-bot-button widget-bot-side-sticky';
|
||||
widgetButton.setAttribute('role', 'button');
|
||||
widgetButton.setAttribute('tabindex', '0');
|
||||
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text}窗口`);
|
||||
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text || '在线客服'}窗口`);
|
||||
widgetButton.setAttribute('data-theme', currentTheme);
|
||||
|
||||
const buttonContent = document.createElement('div');
|
||||
buttonContent.className = 'widget-bot-button-content';
|
||||
|
||||
// 添加logo(如果有)
|
||||
if (widgetInfo.btn_logo) {
|
||||
const logo = document.createElement('img');
|
||||
logo.src = widgetDomain + widgetInfo.btn_logo;
|
||||
logo.alt = 'logo';
|
||||
logo.className = 'widget-bot-logo';
|
||||
logo.onerror = () => {
|
||||
logo.style.display = 'none';
|
||||
};
|
||||
buttonContent.appendChild(logo);
|
||||
}
|
||||
// 侧边吸附显示图标和文字(btn_logo 以及 btn_text)
|
||||
const icon = document.createElement('img');
|
||||
const defaultIconSrc = widgetDomain + '/favicon.png';
|
||||
icon.src = widgetInfo.btn_logo ? (widgetDomain + widgetInfo.btn_logo) : defaultIconSrc;
|
||||
icon.alt = 'icon';
|
||||
icon.className = 'widget-bot-icon';
|
||||
icon.onerror = () => {
|
||||
// 如果当前不是 favicon.png,尝试使用 favicon.png 作为备用
|
||||
if (icon.src !== defaultIconSrc) {
|
||||
icon.src = defaultIconSrc;
|
||||
} else {
|
||||
// 如果 favicon.png 也加载失败,隐藏图标
|
||||
icon.style.display = 'none';
|
||||
}
|
||||
};
|
||||
buttonContent.appendChild(icon);
|
||||
|
||||
// 添加文字
|
||||
const textDiv = document.createElement('div');
|
||||
textDiv.className = 'widget-bot-text';
|
||||
textDiv.innerHTML = createVerticalText(widgetInfo.btn_text || '在线客服');
|
||||
textDiv.innerHTML = createTwoLineText(widgetInfo.btn_text || '在线客服');
|
||||
buttonContent.appendChild(textDiv);
|
||||
|
||||
widgetButton.appendChild(buttonContent);
|
||||
|
||||
// 应用位置 - 距离边缘16px,垂直方向190px
|
||||
const position = widgetInfo.btn_position || defaultBtnPosition;
|
||||
applyButtonPosition(widgetButton, position);
|
||||
|
||||
// 设置 border-radius 为 24px(统一圆角)
|
||||
widgetButton.style.borderRadius = '24px';
|
||||
|
||||
// 添加事件监听器
|
||||
widgetButton.addEventListener('click', showModal);
|
||||
widgetButton.addEventListener('click', handleButtonClick);
|
||||
widgetButton.addEventListener('mousedown', startDrag);
|
||||
widgetButton.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
|
|
@ -134,6 +193,69 @@
|
|||
widgetButton.addEventListener('touchend', handleTouchEnd);
|
||||
|
||||
document.body.appendChild(widgetButton);
|
||||
}
|
||||
|
||||
// 创建悬浮球按钮
|
||||
function createHoverBallButton() {
|
||||
widgetButton = document.createElement('div');
|
||||
widgetButton.className = 'widget-bot-button widget-bot-hover-ball';
|
||||
widgetButton.setAttribute('role', 'button');
|
||||
widgetButton.setAttribute('tabindex', '0');
|
||||
widgetButton.setAttribute('aria-label', `打开${widgetInfo.btn_text || '在线客服'}窗口`);
|
||||
widgetButton.setAttribute('data-theme', currentTheme);
|
||||
|
||||
const buttonContent = document.createElement('div');
|
||||
buttonContent.className = 'widget-bot-button-content';
|
||||
|
||||
// 悬浮球只显示图标(btn_logo)
|
||||
const icon = document.createElement('img');
|
||||
const defaultIconSrc = widgetDomain + '/favicon.png';
|
||||
icon.src = widgetInfo.btn_logo ? (widgetDomain + widgetInfo.btn_logo) : defaultIconSrc;
|
||||
icon.alt = 'icon';
|
||||
icon.className = 'widget-bot-icon widget-bot-hover-ball-icon';
|
||||
icon.onerror = () => {
|
||||
// 如果当前不是 favicon.png,尝试使用 favicon.png 作为备用
|
||||
if (icon.src !== defaultIconSrc) {
|
||||
icon.src = defaultIconSrc;
|
||||
} else {
|
||||
// 如果 favicon.png 也加载失败,隐藏图标
|
||||
icon.style.display = 'none';
|
||||
}
|
||||
};
|
||||
buttonContent.appendChild(icon);
|
||||
|
||||
widgetButton.appendChild(buttonContent);
|
||||
|
||||
// 应用位置 - 距离边缘16px,垂直方向190px
|
||||
applyButtonPosition(widgetButton, widgetInfo.btn_position || defaultBtnPosition);
|
||||
|
||||
// 添加事件监听器
|
||||
widgetButton.addEventListener('click', handleButtonClick);
|
||||
widgetButton.addEventListener('mousedown', startDrag);
|
||||
widgetButton.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
// 添加触摸事件支持
|
||||
widgetButton.addEventListener('touchstart', handleTouchStart, { passive: false });
|
||||
widgetButton.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
widgetButton.addEventListener('touchend', handleTouchEnd);
|
||||
|
||||
document.body.appendChild(widgetButton);
|
||||
}
|
||||
|
||||
// 创建挂件按钮
|
||||
function createWidget() {
|
||||
// 如果已存在,先删除
|
||||
if (widgetButton) {
|
||||
widgetButton.remove();
|
||||
}
|
||||
|
||||
const btnStyle = widgetInfo.btn_style || defaultBtnStyle;
|
||||
|
||||
if (btnStyle === 'hover_ball') {
|
||||
createHoverBallButton();
|
||||
} else {
|
||||
createSideStickyButton();
|
||||
}
|
||||
|
||||
// 创建模态框
|
||||
createModal();
|
||||
|
|
@ -145,6 +267,109 @@
|
|||
}, 100);
|
||||
}
|
||||
|
||||
// 创建自定义触发按钮
|
||||
function createCustomTrigger() {
|
||||
const btnId = widgetInfo.btn_id;
|
||||
if (!btnId) {
|
||||
console.error('btn_trigger 模式需要提供 btn_id');
|
||||
return;
|
||||
}
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 50; // 最多重试 50 次(5秒)
|
||||
|
||||
// 绑定事件到元素
|
||||
function attachTrigger(element) {
|
||||
if (!element) return;
|
||||
|
||||
// 避免重复绑定
|
||||
if (element.hasAttribute('data-widget-trigger-attached')) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.setAttribute('data-widget-trigger-attached', 'true');
|
||||
customTriggerElement = element;
|
||||
|
||||
// 创建事件处理函数并保存引用
|
||||
customTriggerHandler = function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showModal();
|
||||
};
|
||||
|
||||
// 绑定点击事件
|
||||
element.addEventListener('click', customTriggerHandler);
|
||||
}
|
||||
|
||||
// 尝试查找并绑定元素
|
||||
function tryAttachTrigger() {
|
||||
const element = document.getElementById(btnId);
|
||||
if (element) {
|
||||
attachTrigger(element);
|
||||
createModal();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 立即尝试一次
|
||||
if (tryAttachTrigger()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果元素还没加载,使用多种方式监听
|
||||
function retryAttach() {
|
||||
if (tryAttachTrigger()) {
|
||||
return;
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
if (retryCount < maxRetries) {
|
||||
setTimeout(retryAttach, 100);
|
||||
} else {
|
||||
console.warn('自定义触发按钮未找到,已停止重试:', btnId);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 MutationObserver 监听 DOM 变化
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
if (tryAttachTrigger()) {
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// 开始观察 DOM 变化
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// 如果 DOM 已加载完成,立即开始重试
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
setTimeout(retryAttach, 100);
|
||||
});
|
||||
} else {
|
||||
setTimeout(retryAttach, 100);
|
||||
}
|
||||
|
||||
// 延迟断开观察器(避免无限观察)
|
||||
setTimeout(function () {
|
||||
observer.disconnect();
|
||||
}, 10000); // 10秒后断开
|
||||
}
|
||||
|
||||
// 处理按钮点击事件(区分点击和拖拽)
|
||||
function handleButtonClick(e) {
|
||||
// 如果发生了拖拽,不打开弹框
|
||||
if (hasDragged) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
showModal();
|
||||
}
|
||||
|
||||
// 键盘事件处理
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
|
|
@ -176,7 +401,8 @@
|
|||
Math.pow(touch.clientY - touchStartPos.y, 2)
|
||||
);
|
||||
|
||||
if (distance < 10) {
|
||||
// 只有在没有拖拽且移动距离很小的情况下才认为是点击
|
||||
if (!hasDragged && distance < 10) {
|
||||
// 判断为点击事件
|
||||
setTimeout(() => showModal(), 100);
|
||||
}
|
||||
|
|
@ -198,22 +424,41 @@
|
|||
widgetModal.setAttribute('aria-labelledby', 'widget-modal-title');
|
||||
widgetModal.setAttribute('data-theme', currentTheme);
|
||||
|
||||
const modalPosition = widgetInfo.modal_position || defaultModalPosition;
|
||||
if (modalPosition === 'fixed') {
|
||||
widgetModal.classList.add('widget-bot-modal-fixed');
|
||||
}
|
||||
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.className = 'widget-bot-modal-content';
|
||||
if (modalPosition === 'fixed') {
|
||||
modalContent.classList.add('widget-bot-modal-content-fixed');
|
||||
}
|
||||
|
||||
// 创建关闭按钮
|
||||
// 创建关闭按钮(透明框)
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'widget-bot-close-btn';
|
||||
closeBtn.innerHTML = '<svg t="1752218667372" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4632" id="mx_n_1752218667373" width="32" height="32"><path d="M512 939.19762963a427.19762963 427.19762963 0 1 1 0-854.39525926 427.19762963 427.19762963 0 0 1 0 854.39525926z m0-482.08605274L396.47540505 341.53519999a19.41807408 19.41807408 0 0 0-27.44421216 0l-27.44421097 27.44421217a19.41807408 19.41807408 0 0 0 0 27.44421095L457.00801422 512l-115.47281423 115.52459495a19.41807408 19.41807408 0 0 0 0 27.44421216l27.44421217 27.44421097a19.41807408 19.41807408 0 0 0 27.44421095 0L512 566.99198578l115.52459495 115.47281423a19.41807408 19.41807408 0 0 0 27.44421216 0l27.44421097-27.44421217a19.41807408 19.41807408 0 0 0 0-27.44421095l-115.47281424-115.47281423 115.47281424-115.57637689a19.41807408 19.41807408 0 0 0 0-27.44421095l-27.44421097-27.44421096a19.41807408 19.41807408 0 0 0-27.44421216 0L512 457.00801422z" p-id="4633" fill="#ffffff"></path></svg>'
|
||||
closeBtn.setAttribute('aria-label', '关闭窗口');
|
||||
closeBtn.setAttribute('type', 'button');
|
||||
closeBtn.addEventListener('click', hideModal);
|
||||
|
||||
// 创建一个内部元素来处理实际的点击事件(因为按钮设置了 pointer-events: none)
|
||||
const closeBtnArea = document.createElement('div');
|
||||
closeBtnArea.style.width = '100%';
|
||||
closeBtnArea.style.height = '100%';
|
||||
closeBtnArea.style.pointerEvents = 'auto'; // 内部元素可以接收事件
|
||||
closeBtnArea.style.cursor = 'pointer';
|
||||
closeBtnArea.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
hideModal();
|
||||
});
|
||||
closeBtn.appendChild(closeBtnArea);
|
||||
|
||||
// 创建iframe
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'widget-bot-iframe';
|
||||
iframe.src = `${widgetDomain}/widget`;
|
||||
iframe.setAttribute('title', `${widgetInfo.btn_text}服务窗口`);
|
||||
iframe.setAttribute('title', `${widgetInfo.btn_text || '在线客服'}服务窗口`);
|
||||
iframe.setAttribute('allow', 'camera; microphone; geolocation');
|
||||
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-forms allow-popups allow-presentation');
|
||||
|
||||
|
|
@ -224,6 +469,156 @@
|
|||
document.body.appendChild(widgetModal);
|
||||
}
|
||||
|
||||
// 检测是否为移动端
|
||||
function isMobile() {
|
||||
return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
}
|
||||
|
||||
// 智能定位弹框(follow模式)
|
||||
function positionModalFollow(modalContent) {
|
||||
if (!widgetButton || !modalContent) return;
|
||||
|
||||
// 移动端强制居中显示
|
||||
if (isMobile()) {
|
||||
modalContent.style.position = 'relative';
|
||||
modalContent.style.top = 'auto';
|
||||
modalContent.style.left = 'auto';
|
||||
modalContent.style.right = 'auto';
|
||||
modalContent.style.bottom = 'auto';
|
||||
modalContent.style.margin = 'auto';
|
||||
modalContent.style.width = 'calc(100% - 32px)';
|
||||
modalContent.style.height = 'auto';
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const buttonRect = widgetButton.getBoundingClientRect();
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
const margin = 16; // 距离屏幕边缘的最小距离
|
||||
const buttonGap = 16; // 弹框和按钮之间的最小距离
|
||||
|
||||
// 先设置一个临时位置来获取弹框尺寸
|
||||
const originalPosition = modalContent.style.position;
|
||||
const originalTop = modalContent.style.top;
|
||||
const originalLeft = modalContent.style.left;
|
||||
const originalVisibility = modalContent.style.visibility;
|
||||
const originalDisplay = modalContent.style.display;
|
||||
|
||||
modalContent.style.position = 'absolute';
|
||||
modalContent.style.top = '0';
|
||||
modalContent.style.left = '0';
|
||||
modalContent.style.visibility = 'hidden';
|
||||
modalContent.style.display = 'block';
|
||||
|
||||
const modalRect = modalContent.getBoundingClientRect();
|
||||
const modalWidth = modalRect.width;
|
||||
const modalHeight = modalRect.height;
|
||||
|
||||
modalContent.style.visibility = originalVisibility || 'visible';
|
||||
modalContent.style.display = originalDisplay || 'block';
|
||||
|
||||
// 计算按钮中心点
|
||||
const buttonCenterX = buttonRect.left + buttonRect.width / 2;
|
||||
const buttonCenterY = buttonRect.top + buttonRect.height / 2;
|
||||
|
||||
// 判断按钮在屏幕的哪一侧
|
||||
const isLeftSide = buttonCenterX < windowWidth / 2;
|
||||
const isTopSide = buttonCenterY < windowHeight / 2;
|
||||
|
||||
// 智能选择弹框位置,确保完整显示
|
||||
let finalTop, finalBottom, finalLeft, finalRight;
|
||||
|
||||
if (isLeftSide) {
|
||||
// 按钮在左侧,弹框优先显示在右侧(按钮右侧)
|
||||
finalLeft = buttonRect.right + buttonGap;
|
||||
finalRight = 'auto';
|
||||
|
||||
// 如果右侧空间不够,显示在左侧(按钮左侧)
|
||||
if (finalLeft + modalWidth > windowWidth - margin) {
|
||||
finalLeft = 'auto';
|
||||
finalRight = windowWidth - buttonRect.left + buttonGap;
|
||||
// 如果左侧空间也不够,则贴左边(但保持与按钮的距离)
|
||||
if (buttonRect.left - buttonGap - modalWidth < margin) {
|
||||
finalLeft = margin;
|
||||
finalRight = 'auto';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 按钮在右侧,弹框优先显示在左侧(按钮左侧)
|
||||
finalLeft = 'auto';
|
||||
finalRight = windowWidth - buttonRect.left + buttonGap;
|
||||
|
||||
// 如果左侧空间不够,显示在右侧(按钮右侧)
|
||||
if (buttonRect.left - buttonGap - modalWidth < margin) {
|
||||
finalRight = 'auto';
|
||||
finalLeft = buttonRect.right + buttonGap;
|
||||
// 如果右侧空间也不够,则贴右边(但保持与按钮的距离)
|
||||
if (finalLeft + modalWidth > windowWidth - margin) {
|
||||
finalLeft = 'auto';
|
||||
finalRight = margin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 垂直方向:优先与按钮顶部对齐
|
||||
// 弹框顶部与按钮顶部对齐
|
||||
finalTop = buttonRect.top;
|
||||
finalBottom = 'auto';
|
||||
|
||||
// 如果弹框底部超出屏幕,则向上调整,确保弹框完整显示在屏幕内
|
||||
if (finalTop + modalHeight > windowHeight - margin) {
|
||||
// 计算向上调整后的位置
|
||||
const adjustedTop = windowHeight - margin - modalHeight;
|
||||
// 如果调整后的位置仍然在按钮上方,则使用调整后的位置
|
||||
if (adjustedTop >= margin) {
|
||||
finalTop = adjustedTop;
|
||||
} else {
|
||||
// 如果调整后仍然超出,则贴顶部
|
||||
finalTop = margin;
|
||||
}
|
||||
} else if (finalTop < margin) {
|
||||
// 如果弹框顶部超出屏幕,则贴顶部
|
||||
finalTop = margin;
|
||||
}
|
||||
|
||||
// 应用最终位置
|
||||
modalContent.style.top = finalTop !== undefined ? (typeof finalTop === 'string' ? finalTop : finalTop + 'px') : 'auto';
|
||||
modalContent.style.bottom = finalBottom !== undefined ? (typeof finalBottom === 'string' ? finalBottom : finalBottom + 'px') : 'auto';
|
||||
modalContent.style.left = finalLeft !== undefined ? (typeof finalLeft === 'string' ? finalLeft : finalLeft + 'px') : 'auto';
|
||||
modalContent.style.right = finalRight !== undefined ? (typeof finalRight === 'string' ? finalRight : finalRight + 'px') : 'auto';
|
||||
|
||||
// 最终检查并修正,确保弹框完全在屏幕内
|
||||
requestAnimationFrame(() => {
|
||||
const finalModalRect = modalContent.getBoundingClientRect();
|
||||
|
||||
// 修正左边界
|
||||
if (finalModalRect.left < margin) {
|
||||
modalContent.style.left = margin + 'px';
|
||||
modalContent.style.right = 'auto';
|
||||
}
|
||||
|
||||
// 修正右边界
|
||||
if (finalModalRect.right > windowWidth - margin) {
|
||||
modalContent.style.right = margin + 'px';
|
||||
modalContent.style.left = 'auto';
|
||||
}
|
||||
|
||||
// 修正上边界
|
||||
if (finalModalRect.top < margin) {
|
||||
modalContent.style.top = margin + 'px';
|
||||
modalContent.style.bottom = 'auto';
|
||||
}
|
||||
|
||||
// 修正下边界
|
||||
if (finalModalRect.bottom > windowHeight - margin) {
|
||||
modalContent.style.bottom = margin + 'px';
|
||||
modalContent.style.top = 'auto';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 显示模态框
|
||||
function showModal() {
|
||||
if (!widgetModal) return;
|
||||
|
|
@ -231,27 +626,31 @@
|
|||
widgetModal.style.display = 'flex';
|
||||
document.body.classList.add('widget-bot-modal-open');
|
||||
|
||||
// 计算模态框位置
|
||||
requestAnimationFrame(() => {
|
||||
const buttonRect = widgetButton.getBoundingClientRect();
|
||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||
const modalPosition = widgetInfo.modal_position || defaultModalPosition;
|
||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||
|
||||
if (modalContent) {
|
||||
// 设置模态框位置:距离按钮16px,距离底部24px
|
||||
const modalBottom = 24;
|
||||
const modalRight = Math.max(16, window.innerWidth - buttonRect.left + 16);
|
||||
|
||||
modalContent.style.bottom = modalBottom + 'px';
|
||||
modalContent.style.right = modalRight + 'px';
|
||||
|
||||
// 确保模态框不会超出屏幕
|
||||
const modalRect = modalContent.getBoundingClientRect();
|
||||
if (modalRect.left < 16) {
|
||||
modalContent.style.right = '16px';
|
||||
modalContent.style.left = '16px';
|
||||
}
|
||||
}
|
||||
});
|
||||
// 移动端强制居中显示
|
||||
if (isMobile()) {
|
||||
modalContent.style.position = 'relative';
|
||||
modalContent.style.top = 'auto';
|
||||
modalContent.style.left = 'auto';
|
||||
modalContent.style.right = 'auto';
|
||||
modalContent.style.bottom = 'auto';
|
||||
modalContent.style.margin = 'auto';
|
||||
modalContent.style.width = 'calc(100% - 32px)';
|
||||
modalContent.style.height = 'auto';
|
||||
} else if (modalPosition === 'fixed') {
|
||||
// 桌面端固定模式:居中展示
|
||||
modalContent.style.position = 'relative';
|
||||
modalContent.style.top = 'auto';
|
||||
modalContent.style.left = 'auto';
|
||||
modalContent.style.right = 'auto';
|
||||
modalContent.style.bottom = 'auto';
|
||||
modalContent.style.margin = 'auto';
|
||||
} else {
|
||||
// 桌面端跟随模式:跟随按钮位置 - 智能定位,确保弹框完整显示在屏幕内
|
||||
positionModalFollow(modalContent);
|
||||
}
|
||||
|
||||
// 添加ESC键关闭功能
|
||||
document.addEventListener('keydown', handleEscKey);
|
||||
|
|
@ -287,42 +686,98 @@
|
|||
};
|
||||
|
||||
isDragging = true;
|
||||
hasDragged = false; // 重置拖拽标记
|
||||
|
||||
const rect = widgetButton.getBoundingClientRect();
|
||||
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
|
||||
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
|
||||
|
||||
dragOffset.x = clientX - rect.left;
|
||||
dragOffset.y = clientY - rect.top;
|
||||
// 记录拖拽开始位置
|
||||
dragStartPos.x = clientX;
|
||||
dragStartPos.y = clientY;
|
||||
|
||||
// 清除bottom定位,使用top定位
|
||||
widgetButton.style.bottom = 'auto';
|
||||
widgetButton.style.top = rect.top + 'px';
|
||||
// 缓存按钮尺寸,避免拖拽过程中频繁读取
|
||||
buttonSize.width = rect.width;
|
||||
buttonSize.height = rect.height;
|
||||
|
||||
// 先清除 transform,确保获取真实的位置
|
||||
widgetButton.style.transform = 'none';
|
||||
|
||||
// 重新获取位置(清除 transform 后的真实位置)
|
||||
const realRect = widgetButton.getBoundingClientRect();
|
||||
|
||||
// 记录初始位置(基于清除 transform 后的真实位置)
|
||||
initialPosition.left = realRect.left;
|
||||
initialPosition.top = realRect.top;
|
||||
|
||||
dragOffset.x = clientX - realRect.left;
|
||||
dragOffset.y = clientY - realRect.top;
|
||||
|
||||
// 确保使用 fixed 定位,使用真实位置
|
||||
widgetButton.style.position = 'fixed';
|
||||
widgetButton.style.top = realRect.top + 'px';
|
||||
widgetButton.style.left = realRect.left + 'px';
|
||||
widgetButton.style.right = 'auto';
|
||||
widgetButton.style.bottom = 'auto';
|
||||
|
||||
document.addEventListener('mousemove', drag);
|
||||
// 禁用过渡效果,提升拖拽性能
|
||||
widgetButton.style.transition = 'none';
|
||||
|
||||
// 提示浏览器优化(使用 left/top 定位)
|
||||
widgetButton.style.willChange = 'left, top';
|
||||
|
||||
document.addEventListener('mousemove', drag, { passive: false });
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
|
||||
widgetButton.classList.add('dragging');
|
||||
widgetButton.style.zIndex = '10001';
|
||||
}
|
||||
|
||||
// 拖拽中
|
||||
// 拖拽中 - 直接更新位置,实现丝滑跟随
|
||||
function drag(e) {
|
||||
if (!isDragging) return;
|
||||
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault()
|
||||
};
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
const clientX = e.clientX || (e.touches && e.touches[0].clientX);
|
||||
const clientY = e.clientY || (e.touches && e.touches[0].clientY);
|
||||
|
||||
// 检测是否发生了实际移动(超过5px才认为是拖拽)
|
||||
const moveDistance = Math.sqrt(
|
||||
Math.pow(clientX - dragStartPos.x, 2) +
|
||||
Math.pow(clientY - dragStartPos.y, 2)
|
||||
);
|
||||
if (moveDistance > 5) {
|
||||
hasDragged = true;
|
||||
}
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
const buttonWidth = buttonSize.width;
|
||||
const buttonHeight = buttonSize.height;
|
||||
|
||||
// 直接基于鼠标位置计算新位置
|
||||
// 鼠标位置减去拖拽偏移量,得到按钮左上角应该的位置
|
||||
const newLeft = clientX - dragOffset.x;
|
||||
const newTop = clientY - dragOffset.y;
|
||||
const maxTop = window.innerHeight - widgetButton.offsetHeight;
|
||||
|
||||
// 限制在屏幕范围内
|
||||
const constrainedTop = Math.max(0, Math.min(newTop, maxTop));
|
||||
// 垂直位置:限制在屏幕范围内,距离顶部和底部最小距离为 24px
|
||||
const minTop = 24;
|
||||
const maxTop = Math.max(minTop, windowHeight - buttonHeight - 24);
|
||||
const constrainedTop = Math.max(minTop, Math.min(newTop, maxTop));
|
||||
|
||||
// 水平位置:限制在屏幕范围内
|
||||
const maxLeft = windowWidth - buttonWidth;
|
||||
const constrainedLeft = Math.max(0, Math.min(newLeft, maxLeft));
|
||||
|
||||
// 直接使用 left/top 定位,实现无延迟的丝滑跟随
|
||||
// 使用 transform: none 确保不会有任何 transform 干扰
|
||||
widgetButton.style.left = constrainedLeft + 'px';
|
||||
widgetButton.style.top = constrainedTop + 'px';
|
||||
widgetButton.style.right = 'auto';
|
||||
widgetButton.style.bottom = 'auto';
|
||||
widgetButton.style.transform = 'none';
|
||||
}
|
||||
|
||||
// 停止拖拽
|
||||
|
|
@ -330,26 +785,75 @@
|
|||
if (!isDragging) return;
|
||||
|
||||
isDragging = false;
|
||||
|
||||
// 取消待执行的动画帧
|
||||
if (dragAnimationFrame) {
|
||||
cancelAnimationFrame(dragAnimationFrame);
|
||||
dragAnimationFrame = null;
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', drag);
|
||||
document.removeEventListener('mouseup', stopDrag);
|
||||
|
||||
widgetButton.classList.remove('dragging');
|
||||
widgetButton.style.zIndex = '9999';
|
||||
|
||||
// 吸附到右侧,恢复bottom定位
|
||||
// 恢复过渡效果
|
||||
widgetButton.style.transition = '';
|
||||
widgetButton.style.willChange = '';
|
||||
|
||||
// 根据按钮类型和当前位置进行最终定位
|
||||
requestAnimationFrame(() => {
|
||||
const currentTop = parseInt(widgetButton.style.top);
|
||||
const buttonRect = widgetButton.getBoundingClientRect();
|
||||
const currentLeft = buttonRect.left;
|
||||
const currentTop = buttonRect.top;
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
const buttonHeight = widgetButton.offsetHeight;
|
||||
const buttonWidth = buttonSize.width;
|
||||
const buttonHeight = buttonSize.height;
|
||||
|
||||
// 计算距离底部的位置
|
||||
const bottomPosition = windowHeight - currentTop - buttonHeight;
|
||||
// 两种模式使用相同的停止拖拽逻辑:只能左右侧边缘吸附
|
||||
// 根据按钮实际位置判断左右,保持当前位置
|
||||
const screenCenterX = windowWidth / 2;
|
||||
const buttonCenterX = currentLeft + buttonWidth / 2;
|
||||
const isLeftSide = buttonCenterX < screenCenterX;
|
||||
const sideDistance = 16; // 距离边缘的距离
|
||||
|
||||
// 恢复right和bottom定位,清除top
|
||||
widgetButton.style.right = '0';
|
||||
widgetButton.style.bottom = Math.max(20, bottomPosition) + 'px';
|
||||
widgetButton.style.top = 'auto';
|
||||
widgetButton.style.left = 'auto';
|
||||
// 垂直位置:保持在当前位置,限制在屏幕范围内,距离顶部和底部最小距离为 24px
|
||||
const minTop = 24;
|
||||
const maxTop = Math.max(minTop, windowHeight - buttonHeight - 24);
|
||||
const finalTop = Math.max(minTop, Math.min(currentTop, maxTop));
|
||||
let finalLeft;
|
||||
|
||||
// 水平位置:距离左右边16px
|
||||
if (isLeftSide) {
|
||||
finalLeft = sideDistance;
|
||||
widgetButton.style.left = sideDistance + 'px';
|
||||
widgetButton.style.right = 'auto';
|
||||
} else {
|
||||
finalLeft = windowWidth - sideDistance - buttonWidth;
|
||||
widgetButton.style.right = sideDistance + 'px';
|
||||
widgetButton.style.left = 'auto';
|
||||
}
|
||||
|
||||
widgetButton.style.top = finalTop + 'px';
|
||||
widgetButton.style.bottom = 'auto';
|
||||
|
||||
// 清除 transform,使用 left/top 定位
|
||||
widgetButton.style.transform = 'none';
|
||||
|
||||
// 更新 border-radius(现在都是24px圆角)
|
||||
widgetButton.style.borderRadius = '24px';
|
||||
|
||||
// 更新初始位置,为下次拖拽做准备
|
||||
if (finalLeft !== undefined && finalTop !== undefined) {
|
||||
initialPosition.left = finalLeft;
|
||||
initialPosition.top = finalTop;
|
||||
} else {
|
||||
// 如果未定义,使用当前实际位置
|
||||
initialPosition.left = buttonRect.left;
|
||||
initialPosition.top = buttonRect.top;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -390,19 +894,30 @@
|
|||
// 窗口大小改变时重新定位
|
||||
window.addEventListener('resize', function () {
|
||||
if (widgetModal && widgetModal.style.display === 'flex') {
|
||||
// 重新计算模态框位置
|
||||
setTimeout(() => {
|
||||
const buttonRect = widgetButton.getBoundingClientRect();
|
||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||
const modalContent = widgetModal.querySelector('.widget-bot-modal-content');
|
||||
if (!modalContent) return;
|
||||
|
||||
if (modalContent) {
|
||||
const modalBottom = 24;
|
||||
const modalRight = Math.max(16, window.innerWidth - buttonRect.left + 16);
|
||||
// 移动端强制居中显示
|
||||
if (isMobile()) {
|
||||
modalContent.style.position = 'relative';
|
||||
modalContent.style.top = 'auto';
|
||||
modalContent.style.left = 'auto';
|
||||
modalContent.style.right = 'auto';
|
||||
modalContent.style.bottom = 'auto';
|
||||
modalContent.style.margin = 'auto';
|
||||
modalContent.style.width = 'calc(100% - 32px)';
|
||||
modalContent.style.height = 'auto';
|
||||
return;
|
||||
}
|
||||
|
||||
modalContent.style.bottom = modalBottom + 'px';
|
||||
modalContent.style.right = modalRight + 'px';
|
||||
}
|
||||
}, 100);
|
||||
const modalPosition = widgetInfo?.modal_position || defaultModalPosition;
|
||||
if (modalPosition === 'fixed') {
|
||||
// 固定居中模式不需要重新定位
|
||||
return;
|
||||
}
|
||||
|
||||
// 重新计算模态框位置(使用智能定位)
|
||||
positionModalFollow(modalContent);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -423,8 +938,13 @@
|
|||
if (widgetModal) {
|
||||
widgetModal.remove();
|
||||
}
|
||||
if (customTriggerElement && customTriggerHandler) {
|
||||
customTriggerElement.removeEventListener('click', customTriggerHandler);
|
||||
customTriggerElement.removeAttribute('data-widget-trigger-attached');
|
||||
}
|
||||
});
|
||||
|
||||
// 启动
|
||||
init();
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { ThemeStoreProvider } from '@/provider/themeStore';
|
|||
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
||||
import { getShareProV1AuthInfo } from '@/request/pro/ShareAuth';
|
||||
import { Box } from '@mui/material';
|
||||
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
|
||||
import { AppRouterCacheProvider } from '@mui/material-nextjs/v16-appRouter';
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import localFont from 'next/font/local';
|
||||
import { headers, cookies } from 'next/headers';
|
||||
|
|
@ -92,7 +92,7 @@ const Layout = async ({
|
|||
return (
|
||||
<html lang='en'>
|
||||
<body
|
||||
className={`${gilory.variable} ${themeMode === 'dark' ? 'dark' : ''}`}
|
||||
className={`${gilory.variable} ${themeMode === 'dark' ? 'dark' : 'light'}`}
|
||||
>
|
||||
<AppRouterCacheProvider>
|
||||
<ThemeStoreProvider themeMode={themeMode}>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
--color-primary-main: #6e73fe;
|
||||
|
||||
/* 代码块颜色 */
|
||||
--code-bg: #ffffff;
|
||||
--code-bg: rgba(0, 0, 0, 0.03);
|
||||
--code-color: #21222d;
|
||||
--inline-code-bg: #fff5f5;
|
||||
--inline-code-color: #ff502c;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import StoreProvider from '@/provider';
|
||||
import { darkThemeWidget, lightThemeWidget } from '@/theme';
|
||||
import { getShareV1AppWidgetInfo } from '@/request/ShareApp';
|
||||
import { ThemeProvider } from '@ctzhian/ui';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const Layout = async ({
|
||||
|
|
@ -12,18 +9,7 @@ const Layout = async ({
|
|||
}>) => {
|
||||
const widgetDetail: any = await getShareV1AppWidgetInfo();
|
||||
|
||||
const themeMode =
|
||||
widgetDetail?.settings?.widget_bot_settings?.theme_mode || 'light';
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
theme={themeMode === 'dark' ? darkThemeWidget : lightThemeWidget}
|
||||
>
|
||||
<StoreProvider widget={widgetDetail} themeMode={themeMode || 'light'}>
|
||||
{children}
|
||||
</StoreProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
return <StoreProvider widget={widgetDetail}>{children}</StoreProvider>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,3 @@
|
|||
import Widget from '@/views/widget';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<Widget />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
export default Widget;
|
||||
|
|
|
|||
|
|
@ -109,10 +109,19 @@ export type WidgetInfo = {
|
|||
search_placeholder: string;
|
||||
recommend_questions: string[];
|
||||
widget_bot_settings: {
|
||||
btn_logo: string;
|
||||
btn_text: string;
|
||||
is_open: boolean;
|
||||
theme_mode: 'light' | 'dark';
|
||||
btn_logo?: string;
|
||||
btn_text?: string;
|
||||
btn_style?: string;
|
||||
btn_id?: string;
|
||||
btn_position?: string;
|
||||
modal_position?: string;
|
||||
is_open?: boolean;
|
||||
recommend_node_ids?: string[];
|
||||
recommend_questions?: string[];
|
||||
theme_mode?: string;
|
||||
search_mode?: string;
|
||||
placeholder?: string;
|
||||
disclaimer?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ export interface ConversationItem {
|
|||
message_id: string;
|
||||
source: 'history' | 'chat';
|
||||
chunk_result: ChunkResultItem[];
|
||||
result_expend: boolean;
|
||||
thinking_expend: boolean;
|
||||
thinking_content: string;
|
||||
id: string;
|
||||
}
|
||||
|
|
@ -382,6 +384,8 @@ const AiQaContent: React.FC<{
|
|||
const solution = await cap.solve();
|
||||
token = solution.token;
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
setThinking(4);
|
||||
message.error('验证失败');
|
||||
console.log(error, 'error---------');
|
||||
return;
|
||||
|
|
@ -465,6 +469,8 @@ const AiQaContent: React.FC<{
|
|||
if (lastConversation) {
|
||||
lastConversation.a = answerContent;
|
||||
lastConversation.thinking_content = thinkingContent;
|
||||
lastConversation.result_expend = false;
|
||||
lastConversation.thinking_expend = false;
|
||||
}
|
||||
return newConversation;
|
||||
});
|
||||
|
|
@ -513,6 +519,8 @@ const AiQaContent: React.FC<{
|
|||
source: 'chat',
|
||||
chunk_result: [],
|
||||
thinking_content: '',
|
||||
result_expend: true,
|
||||
thinking_expend: true,
|
||||
id: uuidv4(),
|
||||
});
|
||||
messageIdRef.current = '';
|
||||
|
|
@ -631,6 +639,8 @@ const AiQaContent: React.FC<{
|
|||
source: 'history',
|
||||
chunk_result: [],
|
||||
thinking_content: '',
|
||||
result_expend: true,
|
||||
thinking_expend: true,
|
||||
id: uuidv4(),
|
||||
});
|
||||
}
|
||||
|
|
@ -667,6 +677,8 @@ const AiQaContent: React.FC<{
|
|||
chunk_result: [],
|
||||
thinking_content: '',
|
||||
id: uuidv4(),
|
||||
result_expend: true,
|
||||
thinking_expend: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -791,7 +803,16 @@ const AiQaContent: React.FC<{
|
|||
<StyledAiBubble>
|
||||
{/* 搜索结果 */}
|
||||
{item.chunk_result.length > 0 && (
|
||||
<StyledChunkAccordion defaultExpanded>
|
||||
<StyledChunkAccordion
|
||||
expanded={item.result_expend}
|
||||
onChange={(event, expanded) => {
|
||||
setConversation(prev => {
|
||||
const newConversation = [...prev];
|
||||
newConversation[index].result_expend = expanded;
|
||||
return newConversation;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<StyledChunkAccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
|
|
@ -837,7 +858,16 @@ const AiQaContent: React.FC<{
|
|||
|
||||
{/* 思考过程 */}
|
||||
{!!item.thinking_content && (
|
||||
<StyledThinkingAccordion defaultExpanded>
|
||||
<StyledThinkingAccordion
|
||||
expanded={item.thinking_expend}
|
||||
onChange={(event, expanded) => {
|
||||
setConversation(prev => {
|
||||
const newConversation = [...prev];
|
||||
newConversation[index].thinking_expend = expanded;
|
||||
return newConversation;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<StyledThinkingAccordionSummary
|
||||
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
|
|
@ -929,6 +959,9 @@ const AiQaContent: React.FC<{
|
|||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<Box>
|
||||
{kbDetail?.settings?.disclaimer_settings?.content}
|
||||
</Box>
|
||||
</StyledActionStack>
|
||||
)}
|
||||
</StyledAiBubble>
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ export const StyledUserBubble = styled(Box)(({ theme }) => ({
|
|||
|
||||
export const StyledAiBubble = styled(Box)(({ theme }) => ({
|
||||
alignSelf: 'flex-start',
|
||||
maxWidth: '85%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: theme.spacing(3),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -247,7 +247,9 @@ const QaModal: React.FC<QaModalProps> = () => {
|
|||
<Box
|
||||
sx={{
|
||||
px: 3,
|
||||
pt: kbDetail?.settings?.disclaimer_settings?.content ? 2 : 0,
|
||||
pt: kbDetail?.settings?.web_app_custom_style?.show_brand_info
|
||||
? 2
|
||||
: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
|
@ -263,7 +265,10 @@ const QaModal: React.FC<QaModalProps> = () => {
|
|||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box>{kbDetail?.settings?.disclaimer_settings?.content}</Box>
|
||||
<Box>
|
||||
{kbDetail?.settings?.web_app_custom_style?.show_brand_info &&
|
||||
'本网站由 PandaWiki 提供技术支持'}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import Logo from '@/assets/images/logo.png';
|
||||
import { Box } from '@mui/material';
|
||||
import { Stack, Box, IconButton, alpha, Tooltip } from '@mui/material';
|
||||
import { postShareProV1AuthLogout } from '@/request/pro/ShareAuth';
|
||||
import { IconDengchu } from '@panda-wiki/icons';
|
||||
import { useStore } from '@/provider';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import { Modal } from '@ctzhian/ui';
|
||||
import {
|
||||
Header as CustomHeader,
|
||||
WelcomeHeader as WelcomeHeaderComponent,
|
||||
|
|
@ -16,8 +20,58 @@ interface HeaderProps {
|
|||
isWelcomePage?: boolean;
|
||||
}
|
||||
|
||||
const LogoutButton = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleLogout = () => {
|
||||
return postShareProV1AuthLogout().then(() => {
|
||||
// 使用当前页面的协议(http 或 https)
|
||||
const protocol = window.location.protocol;
|
||||
const host = window.location.host;
|
||||
window.location.href = `${protocol}//${host}/auth/login`;
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={
|
||||
<Stack direction='row' alignItems='center' gap={1}>
|
||||
<ErrorIcon sx={{ fontSize: 24, color: 'warning.main' }} />
|
||||
<Box sx={{ mt: '2px' }}>提示</Box>
|
||||
</Stack>
|
||||
}
|
||||
open={open}
|
||||
okText='确定'
|
||||
cancelText='取消'
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={handleLogout}
|
||||
closable={false}
|
||||
>
|
||||
<Box sx={{ pl: 4 }}>确定要退出登录吗?</Box>
|
||||
</Modal>
|
||||
<Tooltip title='退出登录' arrow>
|
||||
<IconButton size='small' onClick={() => setOpen(true)}>
|
||||
<IconDengchu
|
||||
sx={theme => ({
|
||||
cursor: 'pointer',
|
||||
color: alpha(theme.palette.text.primary, 0.65),
|
||||
fontSize: 24,
|
||||
'&:hover': { color: theme.palette.primary.main },
|
||||
})}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = ({ isDocPage = false, isWelcomePage = false }: HeaderProps) => {
|
||||
const { mobile = false, kbDetail, catalogWidth, setQaModalOpen } = useStore();
|
||||
const {
|
||||
mobile = false,
|
||||
kbDetail,
|
||||
catalogWidth,
|
||||
setQaModalOpen,
|
||||
authInfo,
|
||||
} = useStore();
|
||||
const pathname = usePathname();
|
||||
const docWidth = useMemo(() => {
|
||||
if (isWelcomePage) return 'full';
|
||||
|
|
@ -55,16 +109,23 @@ const Header = ({ isDocPage = false, isWelcomePage = false }: HeaderProps) => {
|
|||
onSearch={handleSearch}
|
||||
onQaClick={() => setQaModalOpen?.(true)}
|
||||
>
|
||||
<Box sx={{ ml: 2 }}>
|
||||
<Stack sx={{ ml: 2 }} direction='row' alignItems='center' gap={1}>
|
||||
<ThemeSwitch />
|
||||
</Box>
|
||||
{!!authInfo && <LogoutButton />}
|
||||
</Stack>
|
||||
<QaModal />
|
||||
</CustomHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export const WelcomeHeader = () => {
|
||||
const { mobile = false, kbDetail, catalogWidth, setQaModalOpen } = useStore();
|
||||
const {
|
||||
mobile = false,
|
||||
kbDetail,
|
||||
catalogWidth,
|
||||
setQaModalOpen,
|
||||
authInfo,
|
||||
} = useStore();
|
||||
const handleSearch = (value?: string, type: 'chat' | 'search' = 'chat') => {
|
||||
if (value?.trim()) {
|
||||
if (type === 'chat') {
|
||||
|
|
@ -91,6 +152,7 @@ export const WelcomeHeader = () => {
|
|||
onSearch={handleSearch}
|
||||
onQaClick={() => setQaModalOpen?.(true)}
|
||||
>
|
||||
<Box sx={{ ml: 2 }}>{!!authInfo && <LogoutButton />}</Box>
|
||||
<QaModal />
|
||||
</WelcomeHeaderComponent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import React, { useState, useEffect } from 'react';
|
|||
import { createRoot } from 'react-dom/client';
|
||||
import { styled, SvgIcon, SvgIconProps } from '@mui/material';
|
||||
|
||||
// ==================== 图片数据缓存 ====================
|
||||
// 全局图片 blob URL 缓存,避免重复请求 OSS
|
||||
const imageBlobCache = new Map<string, string>();
|
||||
|
||||
// ==================== 图片数据缓存工具函数 ====================
|
||||
// 下载图片并转换为 blob URL
|
||||
const fetchImageAsBlob = async (src: string): Promise<string> => {
|
||||
const fetchImageAsBlob = async (
|
||||
src: string,
|
||||
imageBlobCache: Map<string, string>,
|
||||
): Promise<string> => {
|
||||
// 检查缓存
|
||||
if (imageBlobCache.has(src)) {
|
||||
return imageBlobCache.get(src)!;
|
||||
|
|
@ -39,12 +39,8 @@ const fetchImageAsBlob = async (src: string): Promise<string> => {
|
|||
}
|
||||
};
|
||||
|
||||
// 导出获取图片 blob URL 的函数
|
||||
export const getImageBlobUrl = (src: string): string | null => {
|
||||
return imageBlobCache.get(src) || null;
|
||||
};
|
||||
|
||||
export const clearImageBlobCache = () => {
|
||||
// 清理图片 blob 缓存
|
||||
export const clearImageBlobCache = (imageBlobCache: Map<string, string>) => {
|
||||
imageBlobCache.forEach(url => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
|
@ -54,7 +50,7 @@ export const clearImageBlobCache = () => {
|
|||
const StyledErrorContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: theme.spacing(2),
|
||||
padding: theme.spacing(1, 6),
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '8px',
|
||||
|
|
@ -71,7 +67,7 @@ const StyledErrorContainer = styled('div')(({ theme }) => ({
|
|||
|
||||
const StyledErrorText = styled('div')(() => ({
|
||||
fontSize: '12px',
|
||||
marginBottom: 16,
|
||||
marginBottom: 10,
|
||||
}));
|
||||
|
||||
export const ImageErrorIcon = (props: SvgIconProps) => {
|
||||
|
|
@ -102,7 +98,7 @@ export const ImageErrorIcon = (props: SvgIconProps) => {
|
|||
const ImageErrorDisplay: React.FC = () => (
|
||||
<StyledErrorContainer>
|
||||
<ImageErrorIcon
|
||||
sx={{ color: 'var(--mui-palette-text-tertiary)', fontSize: 160 }}
|
||||
sx={{ color: 'var(--mui-palette-text-tertiary)', fontSize: 140 }}
|
||||
/>
|
||||
<StyledErrorText>图片加载失败</StyledErrorText>
|
||||
</StyledErrorContainer>
|
||||
|
|
@ -116,7 +112,7 @@ interface ImageComponentProps {
|
|||
imageIndex: number;
|
||||
onLoad: (index: number, html: string) => void;
|
||||
onError: (index: number, html: string) => void;
|
||||
onImageClick: (src: string) => void;
|
||||
imageBlobCache: Map<string, string>;
|
||||
}
|
||||
|
||||
// ==================== 图片组件 ====================
|
||||
|
|
@ -127,7 +123,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
|||
imageIndex,
|
||||
onLoad,
|
||||
onError,
|
||||
onImageClick,
|
||||
imageBlobCache,
|
||||
}) => {
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>(
|
||||
'loading',
|
||||
|
|
@ -149,7 +145,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
|||
// 获取图片 blob URL
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
fetchImageAsBlob(src)
|
||||
fetchImageAsBlob(src, imageBlobCache)
|
||||
.then(url => {
|
||||
if (mounted) {
|
||||
setBlobUrl(url);
|
||||
|
|
@ -166,7 +162,7 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
|||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [src]);
|
||||
}, [src, imageBlobCache]);
|
||||
|
||||
// 解析自定义样式
|
||||
const parseStyleString = (styleStr: string) => {
|
||||
|
|
@ -238,7 +234,8 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
|||
referrerPolicy='no-referrer'
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
onClick={() => onImageClick(src)} // 传递原始 src 用于预览
|
||||
data-original-src={src}
|
||||
className='markdown-image'
|
||||
{...getOtherProps()}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -264,12 +261,13 @@ const ImageComponent: React.FC<ImageComponentProps> = ({
|
|||
export interface ImageRendererOptions {
|
||||
onImageLoad: (index: number, html: string) => void;
|
||||
onImageError: (index: number, html: string) => void;
|
||||
onImageClick: (src: string) => void;
|
||||
imageRenderCache: Map<number, string>;
|
||||
imageBlobCache: Map<string, string>;
|
||||
}
|
||||
|
||||
export const createImageRenderer = (options: ImageRendererOptions) => {
|
||||
const { onImageLoad, onImageError, imageRenderCache, onImageClick } = options;
|
||||
const { onImageLoad, onImageError, imageRenderCache, imageBlobCache } =
|
||||
options;
|
||||
return (
|
||||
src: string,
|
||||
alt: string,
|
||||
|
|
@ -279,29 +277,6 @@ export const createImageRenderer = (options: ImageRendererOptions) => {
|
|||
// 检查缓存
|
||||
const cached = imageRenderCache.get(imageIndex);
|
||||
if (cached) {
|
||||
// 下一帧对已缓存的DOM绑定原生点击事件,避免事件丢失且不引起重渲染
|
||||
requestAnimationFrame(() => {
|
||||
const container = document.querySelector(
|
||||
`.image-container-${imageIndex}`,
|
||||
) as HTMLElement | null;
|
||||
if (!container) return;
|
||||
const img = container.querySelector('img') as HTMLImageElement | null;
|
||||
if (!img) return;
|
||||
const alreadyBound = (img as HTMLElement).getAttribute(
|
||||
'data-click-bound',
|
||||
);
|
||||
if (!alreadyBound) {
|
||||
(img as HTMLElement).setAttribute('data-click-bound', '1');
|
||||
img.style.cursor = img.style.cursor || 'pointer';
|
||||
img.addEventListener('click', () => {
|
||||
try {
|
||||
onImageClick(img.src);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return cached;
|
||||
}
|
||||
|
||||
|
|
@ -323,7 +298,7 @@ export const createImageRenderer = (options: ImageRendererOptions) => {
|
|||
imageIndex={imageIndex}
|
||||
onLoad={onImageLoad}
|
||||
onError={onImageError}
|
||||
onImageClick={onImageClick}
|
||||
imageBlobCache={imageBlobCache}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -15,11 +15,7 @@ import React, {
|
|||
useState,
|
||||
} from 'react';
|
||||
import { useSmartScroll } from '@/hooks';
|
||||
import {
|
||||
clearImageBlobCache,
|
||||
createImageRenderer,
|
||||
getImageBlobUrl,
|
||||
} from './imageRenderer';
|
||||
import { clearImageBlobCache, createImageRenderer } from './imageRenderer';
|
||||
import { incrementalRender } from './incrementalRenderer';
|
||||
import { createMermaidRenderer } from './mermaidRenderer';
|
||||
import {
|
||||
|
|
@ -88,7 +84,8 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
const lastContentRef = useRef<string>('');
|
||||
const mdRef = useRef<MarkdownIt | null>(null);
|
||||
const mermaidSuccessIdRef = useRef<Map<number, string>>(new Map());
|
||||
const imageRenderCacheRef = useRef<Map<number, string>>(new Map()); // 图片渲染缓存
|
||||
const imageRenderCacheRef = useRef<Map<number, string>>(new Map()); // 图片渲染缓存(HTML)
|
||||
const imageBlobCacheRef = useRef<Map<string, string>>(new Map()); // 图片 blob URL 缓存
|
||||
|
||||
// 使用智能滚动 hook
|
||||
const { scrollToBottom } = useSmartScroll({
|
||||
|
|
@ -125,13 +122,8 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
createImageRenderer({
|
||||
onImageLoad: handleImageLoad,
|
||||
onImageError: handleImageError,
|
||||
onImageClick: (src: string) => {
|
||||
// 尝试获取缓存的 blob URL,如果不存在则使用原始 src
|
||||
const blobUrl = getImageBlobUrl(src);
|
||||
setPreviewImgBlobUrl(blobUrl || src);
|
||||
setPreviewOpen(true);
|
||||
},
|
||||
imageRenderCache: imageRenderCacheRef.current,
|
||||
imageBlobCache: imageBlobCacheRef.current,
|
||||
}),
|
||||
[handleImageLoad, handleImageError],
|
||||
);
|
||||
|
|
@ -158,6 +150,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
const originalFenceRender = md.renderer.rules.fence;
|
||||
// 自定义图片渲染
|
||||
let imageCount = 0;
|
||||
let htmlImageCount = 0; // HTML 标签图片计数
|
||||
let mermaidCount = 0;
|
||||
md.renderer.rules.image = (tokens, idx) => {
|
||||
imageCount++;
|
||||
|
|
@ -240,6 +233,38 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
);
|
||||
};
|
||||
|
||||
// 解析 HTML img 标签并提取属性
|
||||
const parseImgTag = (
|
||||
html: string,
|
||||
): {
|
||||
src: string;
|
||||
alt: string;
|
||||
attrs: [string, string][];
|
||||
} | null => {
|
||||
// 匹配 <img> 标签(支持自闭合和普通标签)
|
||||
const imgMatch = html.match(/<img\s+([^>]*?)\/?>/i);
|
||||
if (!imgMatch) return null;
|
||||
|
||||
const attrsString = imgMatch[1];
|
||||
const attrs: [string, string][] = [];
|
||||
let src = '';
|
||||
let alt = '';
|
||||
|
||||
// 解析属性:匹配 name="value" 或 name='value' 或 name=value
|
||||
const attrRegex =
|
||||
/(\w+)(?:=["']([^"']*)["']|=(?:["'])?([^\s>]+)(?:["'])?)?/g;
|
||||
let attrMatch;
|
||||
while ((attrMatch = attrRegex.exec(attrsString)) !== null) {
|
||||
const name = attrMatch[1].toLowerCase();
|
||||
const value = attrMatch[2] || attrMatch[3] || '';
|
||||
attrs.push([name, value]);
|
||||
if (name === 'src') src = value;
|
||||
if (name === 'alt') alt = value;
|
||||
}
|
||||
|
||||
return { src, alt, attrs };
|
||||
};
|
||||
|
||||
md.renderer.rules.html_block = (
|
||||
tokens,
|
||||
idx,
|
||||
|
|
@ -278,6 +303,21 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
if (content.includes('<error>')) return '<span class="chat-error">';
|
||||
if (content.includes('</error>')) return '</span>';
|
||||
|
||||
// 处理 img 标签
|
||||
if (content.includes('<img')) {
|
||||
const imgData = parseImgTag(content);
|
||||
if (imgData && imgData.src) {
|
||||
const imageIndex = imageCount + htmlImageCount;
|
||||
htmlImageCount++;
|
||||
return renderImage(
|
||||
imgData.src,
|
||||
imgData.alt,
|
||||
imgData.attrs,
|
||||
imageIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔒 安全检查:不在白名单的标签,转义输出
|
||||
if (!isAllowedTag(content)) {
|
||||
return md.utils.escapeHtml(content);
|
||||
|
|
@ -301,6 +341,21 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
if (content.includes('<error>')) return '<span class="chat-error">';
|
||||
if (content.includes('</error>')) return '</span>';
|
||||
|
||||
// 处理 img 标签
|
||||
if (content.includes('<img')) {
|
||||
const imgData = parseImgTag(content);
|
||||
if (imgData && imgData.src) {
|
||||
const imageIndex = imageCount + htmlImageCount;
|
||||
htmlImageCount++;
|
||||
return renderImage(
|
||||
imgData.src,
|
||||
imgData.alt,
|
||||
imgData.attrs,
|
||||
imageIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔒 安全检查:不在白名单的标签,转义输出
|
||||
if (!isAllowedTag(content)) {
|
||||
return md.utils.escapeHtml(content);
|
||||
|
|
@ -352,7 +407,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
}
|
||||
}, [content, customizeRenderer, scrollToBottom]);
|
||||
|
||||
// 添加代码块点击复制功能
|
||||
// 添加代码块点击复制和图片点击预览功能(事件代理)
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
|
@ -360,6 +415,21 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
const handleClick = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// 检查是否点击了图片
|
||||
const imgElement = target.closest(
|
||||
'img.markdown-image',
|
||||
) as HTMLImageElement;
|
||||
if (imgElement) {
|
||||
const originalSrc = imgElement.getAttribute('data-original-src');
|
||||
if (originalSrc) {
|
||||
// 尝试获取缓存的 blob URL,如果不存在则使用原始 src
|
||||
const blobUrl = imageBlobCacheRef.current.get(originalSrc);
|
||||
setPreviewImgBlobUrl(blobUrl || originalSrc);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否点击了代码块
|
||||
const preElement = target.closest('pre.hljs');
|
||||
if (preElement) {
|
||||
|
|
@ -368,6 +438,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
const code = codeElement.textContent || '';
|
||||
copyText(code.replace(/\n$/, ''));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否点击了行内代码
|
||||
|
|
@ -380,7 +451,7 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
container.addEventListener('click', handleClick);
|
||||
|
||||
return () => {
|
||||
clearImageBlobCache();
|
||||
clearImageBlobCache(imageBlobCacheRef.current);
|
||||
container.removeEventListener('click', handleClick);
|
||||
};
|
||||
}, []);
|
||||
|
|
@ -406,6 +477,9 @@ const MarkDown2: React.FC<MarkDown2Props> = ({
|
|||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
},
|
||||
'.markdown-image': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'.image-error': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getShareV1AppWidgetInfo } from './request/ShareApp';
|
||||
import { middleware as homeMiddleware } from './middleware/home';
|
||||
|
||||
const proxyShare = async (request: NextRequest) => {
|
||||
// 转发到 process.env.TARGET
|
||||
const kb_id = request.headers.get('x-kb-id') || process.env.DEV_KB_ID || '';
|
||||
|
||||
const targetOrigin = process.env.TARGET!;
|
||||
const targetUrl = new URL(
|
||||
request.nextUrl.pathname + request.nextUrl.search,
|
||||
targetOrigin,
|
||||
);
|
||||
// 构造 fetch 选项
|
||||
const fetchHeaders = new Headers(request.headers);
|
||||
fetchHeaders.set('x-kb-id', kb_id);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: request.method,
|
||||
headers: fetchHeaders,
|
||||
body: ['GET', 'HEAD'].includes(request.method) ? undefined : request.body,
|
||||
redirect: 'manual',
|
||||
};
|
||||
const proxyRes = await fetch(targetUrl.toString(), fetchOptions);
|
||||
const nextRes = new NextResponse(proxyRes.body, {
|
||||
status: proxyRes.status,
|
||||
headers: proxyRes.headers,
|
||||
statusText: proxyRes.statusText,
|
||||
});
|
||||
return nextRes;
|
||||
};
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const url = request.nextUrl.clone();
|
||||
const pathname = url.pathname;
|
||||
if (pathname.startsWith('/widget')) {
|
||||
const widgetInfo: any = await getShareV1AppWidgetInfo();
|
||||
if (widgetInfo) {
|
||||
if (!widgetInfo?.settings?.widget_bot_settings?.is_open) {
|
||||
return NextResponse.rewrite(new URL('/not-fount', request.url));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [key, value] of request.headers.entries()) {
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
let sessionId = request.cookies.get('x-pw-session-id')?.value || '';
|
||||
let needSetSessionId = false;
|
||||
|
||||
if (!sessionId) {
|
||||
sessionId = uuidv4();
|
||||
needSetSessionId = true;
|
||||
}
|
||||
|
||||
let response: NextResponse;
|
||||
|
||||
if (pathname.startsWith('/share/')) {
|
||||
response = await proxyShare(request);
|
||||
} else {
|
||||
response = await homeMiddleware(request, headers, sessionId);
|
||||
}
|
||||
|
||||
if (needSetSessionId) {
|
||||
response.cookies.set('x-pw-session-id', sessionId, {
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 年
|
||||
});
|
||||
}
|
||||
if (!pathname.startsWith('/share')) {
|
||||
response.headers.set('x-current-path', pathname);
|
||||
response.headers.set('x-current-search', url.search);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/',
|
||||
'/home',
|
||||
'/share/:path*',
|
||||
'/chat/:path*',
|
||||
'/widget',
|
||||
'/welcome',
|
||||
'/auth/login',
|
||||
'/node/:path*',
|
||||
'/node',
|
||||
// '/client/:path*',
|
||||
],
|
||||
};
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import { parsePathname } from '@/utils';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { postShareV1StatPage } from '@/request/ShareStat';
|
||||
import { getShareV1NodeList } from '@/request/ShareNode';
|
||||
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
||||
import { filterEmptyFolders, convertToTree } from '@/utils/drag';
|
||||
import { deepSearchFirstNode } from '@/utils';
|
||||
|
||||
const StatPage = {
|
||||
welcome: 1,
|
||||
node: 2,
|
||||
chat: 3,
|
||||
auth: 4,
|
||||
} as const;
|
||||
|
||||
const getFirstNode = async () => {
|
||||
const nodeListResult: any = await getShareV1NodeList();
|
||||
const tree = filterEmptyFolders(convertToTree(nodeListResult || []));
|
||||
return deepSearchFirstNode(tree);
|
||||
};
|
||||
|
||||
const getHomePath = async () => {
|
||||
const info = await getShareV1AppWebInfo();
|
||||
return info?.settings?.home_page_setting;
|
||||
};
|
||||
|
||||
export async function middleware(
|
||||
request: NextRequest,
|
||||
headers: Record<string, string>,
|
||||
session: string,
|
||||
) {
|
||||
const url = request.nextUrl.clone();
|
||||
const { page, id } = parsePathname(url.pathname);
|
||||
try {
|
||||
// 获取节点列表
|
||||
if (url.pathname === '/') {
|
||||
const homePath = await getHomePath();
|
||||
if (homePath === 'custom') {
|
||||
return NextResponse.rewrite(new URL('/home', request.url));
|
||||
} else {
|
||||
const [firstNode] = await Promise.all([getFirstNode(), getHomePath()]);
|
||||
if (firstNode) {
|
||||
return NextResponse.rewrite(
|
||||
new URL(`/node/${firstNode.id}`, request.url),
|
||||
);
|
||||
}
|
||||
return NextResponse.rewrite(new URL('/node', request.url));
|
||||
}
|
||||
}
|
||||
|
||||
// 页面上报
|
||||
const pages = Object.keys(StatPage);
|
||||
if (pages.includes(page) || pages.includes(id)) {
|
||||
postShareV1StatPage(
|
||||
{
|
||||
scene: StatPage[page as keyof typeof StatPage],
|
||||
node_id: id || '',
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'x-pw-session-id': session,
|
||||
...headers,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
} catch (error) {
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'message' in error &&
|
||||
error.message === 'NEXT_REDIRECT'
|
||||
) {
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
`/auth/login?redirect=${encodeURIComponent(url.pathname + url.search)}`,
|
||||
request.url,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
|
@ -35,6 +35,10 @@ export const ThemeStoreProvider = ({
|
|||
useEffect(() => {
|
||||
Cookies.set('theme_mode', themeMode, { expires: 365 * 10 });
|
||||
}, [themeMode]);
|
||||
|
||||
console.log('themeMode-------', themeMode);
|
||||
console.log('themeMode-------', theme);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ themeMode, setThemeMode }}>
|
||||
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
import type { NextRequest } from 'next/server';
|
||||
import { NextResponse } from 'next/server';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getShareV1AppWidgetInfo } from './request/ShareApp';
|
||||
|
||||
import { parsePathname } from '@/utils';
|
||||
import { postShareV1StatPage } from '@/request/ShareStat';
|
||||
import { getShareV1NodeList } from '@/request/ShareNode';
|
||||
import { getShareV1AppWebInfo } from '@/request/ShareApp';
|
||||
import { filterEmptyFolders, convertToTree } from '@/utils/drag';
|
||||
import { deepSearchFirstNode } from '@/utils';
|
||||
|
||||
const StatPage = {
|
||||
welcome: 1,
|
||||
node: 2,
|
||||
chat: 3,
|
||||
auth: 4,
|
||||
} as const;
|
||||
|
||||
const getFirstNode = async () => {
|
||||
const nodeListResult: any = await getShareV1NodeList();
|
||||
const tree = filterEmptyFolders(convertToTree(nodeListResult || []));
|
||||
return deepSearchFirstNode(tree);
|
||||
};
|
||||
|
||||
const getHomePath = async () => {
|
||||
const info = await getShareV1AppWebInfo();
|
||||
return info?.settings?.home_page_setting;
|
||||
};
|
||||
|
||||
const homeProxy = async (
|
||||
request: NextRequest,
|
||||
headers: Record<string, string>,
|
||||
session: string,
|
||||
) => {
|
||||
const url = request.nextUrl.clone();
|
||||
const { page, id } = parsePathname(url.pathname);
|
||||
try {
|
||||
// 获取节点列表
|
||||
if (url.pathname === '/') {
|
||||
const homePath = await getHomePath();
|
||||
if (homePath === 'custom') {
|
||||
return NextResponse.rewrite(new URL('/home', request.url));
|
||||
} else {
|
||||
const [firstNode] = await Promise.all([getFirstNode(), getHomePath()]);
|
||||
if (firstNode) {
|
||||
return NextResponse.rewrite(
|
||||
new URL(`/node/${firstNode.id}`, request.url),
|
||||
);
|
||||
}
|
||||
return NextResponse.rewrite(new URL('/node', request.url));
|
||||
}
|
||||
}
|
||||
|
||||
// 页面上报
|
||||
const pages = Object.keys(StatPage);
|
||||
if (pages.includes(page) || pages.includes(id)) {
|
||||
postShareV1StatPage(
|
||||
{
|
||||
scene: StatPage[page as keyof typeof StatPage],
|
||||
node_id: id || '',
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'x-pw-session-id': session,
|
||||
...headers,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
} catch (error) {
|
||||
if (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'message' in error &&
|
||||
error.message === 'NEXT_REDIRECT'
|
||||
) {
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
`/auth/login?redirect=${encodeURIComponent(url.pathname + url.search)}`,
|
||||
request.url,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
};
|
||||
|
||||
const proxyShare = async (request: NextRequest) => {
|
||||
// 转发到 process.env.TARGET
|
||||
const kb_id = request.headers.get('x-kb-id') || process.env.DEV_KB_ID || '';
|
||||
|
||||
const targetOrigin = process.env.TARGET!;
|
||||
const targetUrl = new URL(
|
||||
request.nextUrl.pathname + request.nextUrl.search,
|
||||
targetOrigin,
|
||||
);
|
||||
// 构造 fetch 选项
|
||||
const fetchHeaders = new Headers(request.headers);
|
||||
fetchHeaders.set('x-kb-id', kb_id);
|
||||
|
||||
const hasBody = !['GET', 'HEAD'].includes(request.method);
|
||||
const fetchOptions: RequestInit = {
|
||||
method: request.method,
|
||||
headers: fetchHeaders,
|
||||
body: hasBody ? request.body : undefined,
|
||||
redirect: 'manual',
|
||||
...(hasBody && { duplex: 'half' as const }),
|
||||
};
|
||||
const proxyRes = await fetch(targetUrl.toString(), fetchOptions);
|
||||
const nextRes = new NextResponse(proxyRes.body, {
|
||||
status: proxyRes.status,
|
||||
headers: proxyRes.headers,
|
||||
statusText: proxyRes.statusText,
|
||||
});
|
||||
return nextRes;
|
||||
};
|
||||
|
||||
export async function proxy(request: NextRequest) {
|
||||
const url = request.nextUrl.clone();
|
||||
const pathname = url.pathname;
|
||||
if (pathname.startsWith('/widget')) {
|
||||
const widgetInfo: any = await getShareV1AppWidgetInfo();
|
||||
if (widgetInfo) {
|
||||
if (!widgetInfo?.settings?.widget_bot_settings?.is_open) {
|
||||
return NextResponse.rewrite(new URL('/not-found', request.url));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
for (const [key, value] of request.headers.entries()) {
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
let sessionId = request.cookies.get('x-pw-session-id')?.value || '';
|
||||
let needSetSessionId = false;
|
||||
|
||||
if (!sessionId) {
|
||||
sessionId = uuidv4();
|
||||
needSetSessionId = true;
|
||||
}
|
||||
|
||||
let response: NextResponse;
|
||||
|
||||
if (pathname.startsWith('/share/')) {
|
||||
response = await proxyShare(request);
|
||||
} else {
|
||||
response = await homeProxy(request, headers, sessionId);
|
||||
}
|
||||
|
||||
if (needSetSessionId) {
|
||||
response.cookies.set('x-pw-session-id', sessionId, {
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 60 * 24 * 365, // 1 年
|
||||
});
|
||||
}
|
||||
if (!pathname.startsWith('/share')) {
|
||||
response.headers.set('x-current-path', pathname);
|
||||
response.headers.set('x-current-search', url.search);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/',
|
||||
'/home',
|
||||
'/share/:path*',
|
||||
'/chat/:path*',
|
||||
'/widget',
|
||||
'/welcome',
|
||||
'/auth/login',
|
||||
'/node/:path*',
|
||||
'/node',
|
||||
],
|
||||
};
|
||||
|
|
@ -18,7 +18,6 @@ import {
|
|||
DomainOpenAICompletionsResponse,
|
||||
DomainResponse,
|
||||
PostShareV1ChatMessageParams,
|
||||
PostShareV1ChatWidgetParams,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
|
|
@ -92,28 +91,3 @@ export const postShareV1ChatMessage = (
|
|||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description ChatWidget
|
||||
*
|
||||
* @tags share_chat
|
||||
* @name PostShareV1ChatWidget
|
||||
* @summary ChatWidget
|
||||
* @request POST:/share/v1/chat/widget
|
||||
* @response `200` `DomainResponse` OK
|
||||
*/
|
||||
|
||||
export const postShareV1ChatWidget = (
|
||||
query: PostShareV1ChatWidgetParams,
|
||||
request: DomainChatRequest,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<DomainResponse>({
|
||||
path: `/share/v1/chat/widget`,
|
||||
method: "POST",
|
||||
query: query,
|
||||
body: request,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
||||
* ## ##
|
||||
* ## AUTHOR: acacode ##
|
||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
||||
* ---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import httpRequest, { ContentType, RequestParams } from "./httpClient";
|
||||
import {
|
||||
DomainChatRequest,
|
||||
DomainChatSearchReq,
|
||||
DomainChatSearchResp,
|
||||
DomainResponse,
|
||||
PostShareV1ChatWidgetParams,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* @description ChatWidget
|
||||
*
|
||||
* @tags Widget
|
||||
* @name PostShareV1ChatWidget
|
||||
* @summary ChatWidget
|
||||
* @request POST:/share/v1/chat/widget
|
||||
* @response `200` `DomainResponse` OK
|
||||
*/
|
||||
|
||||
export const postShareV1ChatWidget = (
|
||||
query: PostShareV1ChatWidgetParams,
|
||||
request: DomainChatRequest,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<DomainResponse>({
|
||||
path: `/share/v1/chat/widget`,
|
||||
method: "POST",
|
||||
query: query,
|
||||
body: request,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description WidgetSearch
|
||||
*
|
||||
* @tags Widget
|
||||
* @name PostShareV1ChatWidgetSearch
|
||||
* @summary WidgetSearch
|
||||
* @request POST:/share/v1/chat/widget/search
|
||||
* @response `200` `(DomainResponse & {
|
||||
data?: DomainChatSearchResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postShareV1ChatWidgetSearch = (
|
||||
request: DomainChatSearchReq,
|
||||
params: RequestParams = {},
|
||||
) =>
|
||||
httpRequest<
|
||||
DomainResponse & {
|
||||
data?: DomainChatSearchResp;
|
||||
}
|
||||
>({
|
||||
path: `/share/v1/chat/widget/search`,
|
||||
method: "POST",
|
||||
body: request,
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
|
@ -10,5 +10,6 @@ export * from './ShareNode'
|
|||
export * from './ShareOpenapi'
|
||||
export * from './ShareStat'
|
||||
export * from './Wechat'
|
||||
export * from './Widget'
|
||||
export * from './types'
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
GithubComChaitinPandaWikiProApiShareV1AuthInfoResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPReq,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthOAuthResp,
|
||||
GithubComChaitinPandaWikiProApiShareV1AuthWecomReq,
|
||||
|
|
@ -206,6 +207,32 @@ export const postShareProV1AuthLdap = (
|
|||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 用户登出
|
||||
*
|
||||
* @tags ShareAuth
|
||||
* @name PostShareProV1AuthLogout
|
||||
* @summary 用户登出
|
||||
* @request POST:/share/pro/v1/auth/logout
|
||||
* @response `200` `(DomainPWResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp,
|
||||
|
||||
})` OK
|
||||
*/
|
||||
|
||||
export const postShareProV1AuthLogout = (params: RequestParams = {}) =>
|
||||
httpRequest<
|
||||
DomainPWResponse & {
|
||||
data?: GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp;
|
||||
}
|
||||
>({
|
||||
path: `/share/pro/v1/auth/logout`,
|
||||
method: "POST",
|
||||
type: ContentType.Json,
|
||||
format: "json",
|
||||
...params,
|
||||
});
|
||||
|
||||
/**
|
||||
* @description OAuth登录
|
||||
*
|
||||
|
|
|
|||
|
|
@ -52,10 +52,12 @@ export enum ConstsSourceType {
|
|||
export enum ConstsLicenseEdition {
|
||||
/** 开源版 */
|
||||
LicenseEditionFree = 0,
|
||||
/** 联创版 */
|
||||
LicenseEditionContributor = 1,
|
||||
/** 专业版 */
|
||||
LicenseEditionProfession = 1,
|
||||
/** 企业版 */
|
||||
LicenseEditionEnterprise = 2,
|
||||
/** 商业版 */
|
||||
LicenseEditionBusiness = 3,
|
||||
}
|
||||
|
||||
export enum ConstsContributeType {
|
||||
|
|
@ -455,6 +457,11 @@ export type GithubComChaitinPandaWikiProApiShareV1AuthLDAPResp = Record<
|
|||
any
|
||||
>;
|
||||
|
||||
export type GithubComChaitinPandaWikiProApiShareV1AuthLogoutResp = Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
|
||||
export interface GithubComChaitinPandaWikiProApiShareV1AuthOAuthReq {
|
||||
kb_id?: string;
|
||||
redirect_url?: string;
|
||||
|
|
@ -669,8 +676,6 @@ export interface GetApiProV1TokenListParams {
|
|||
}
|
||||
|
||||
export interface PostApiV1LicensePayload {
|
||||
/** license edition */
|
||||
license_edition: "contributor" | "enterprise";
|
||||
/** license type */
|
||||
license_type: "file" | "code";
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -171,14 +171,21 @@ export enum ConstsNodeAccessPerm {
|
|||
NodeAccessPermClosed = "closed",
|
||||
}
|
||||
|
||||
export enum ConstsModelSettingMode {
|
||||
ModelSettingModeManual = "manual",
|
||||
ModelSettingModeAuto = "auto",
|
||||
}
|
||||
|
||||
/** @format int32 */
|
||||
export enum ConstsLicenseEdition {
|
||||
/** 开源版 */
|
||||
LicenseEditionFree = 0,
|
||||
/** 联创版 */
|
||||
LicenseEditionContributor = 1,
|
||||
/** 专业版 */
|
||||
LicenseEditionProfession = 1,
|
||||
/** 企业版 */
|
||||
LicenseEditionEnterprise = 2,
|
||||
/** 商业版 */
|
||||
LicenseEditionBusiness = 3,
|
||||
}
|
||||
|
||||
export enum ConstsHomePageSetting {
|
||||
|
|
@ -922,6 +929,17 @@ export interface DomainMetricsConfig {
|
|||
type?: string;
|
||||
}
|
||||
|
||||
export interface DomainModelModeSetting {
|
||||
/** 百智云 API Key */
|
||||
auto_mode_api_key?: string;
|
||||
/** 自定义对话模型名称 */
|
||||
chat_model?: string;
|
||||
/** 手动模式下嵌入模型是否更新 */
|
||||
is_manual_embedding_updated?: boolean;
|
||||
/** 模式: manual 或 auto */
|
||||
mode?: ConstsModelSettingMode;
|
||||
}
|
||||
|
||||
export interface DomainMoveNodeReq {
|
||||
id: string;
|
||||
kb_id: string;
|
||||
|
|
@ -1171,6 +1189,17 @@ export interface DomainShareConversationMessage {
|
|||
role?: SchemaRoleType;
|
||||
}
|
||||
|
||||
export interface DomainShareNodeListItemResp {
|
||||
emoji?: string;
|
||||
id?: string;
|
||||
name?: string;
|
||||
parent_id?: string;
|
||||
permissions?: DomainNodePermissions;
|
||||
position?: number;
|
||||
type?: DomainNodeType;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface DomainSimpleAuth {
|
||||
enabled?: boolean;
|
||||
password?: string;
|
||||
|
|
@ -1195,6 +1224,18 @@ export interface DomainStatPageReq {
|
|||
scene: 1 | 2 | 3 | 4;
|
||||
}
|
||||
|
||||
export interface DomainSwitchModeReq {
|
||||
/** 百智云 API Key */
|
||||
auto_mode_api_key?: string;
|
||||
/** 自定义对话模型名称 */
|
||||
chat_model?: string;
|
||||
mode: "manual" | "auto";
|
||||
}
|
||||
|
||||
export interface DomainSwitchModeResp {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface DomainTextConfig {
|
||||
title?: string;
|
||||
type?: string;
|
||||
|
|
@ -1336,11 +1377,18 @@ export interface DomainWecomAIBotSettings {
|
|||
}
|
||||
|
||||
export interface DomainWidgetBotSettings {
|
||||
btn_id?: string;
|
||||
btn_logo?: string;
|
||||
btn_position?: string;
|
||||
btn_style?: string;
|
||||
btn_text?: string;
|
||||
disclaimer?: string;
|
||||
is_open?: boolean;
|
||||
modal_position?: string;
|
||||
placeholder?: string;
|
||||
recommend_node_ids?: string[];
|
||||
recommend_questions?: string[];
|
||||
search_mode?: string;
|
||||
theme_mode?: string;
|
||||
}
|
||||
|
||||
|
|
@ -1646,6 +1694,7 @@ export interface V1ShareNodeDetailResp {
|
|||
editor_id?: string;
|
||||
id?: string;
|
||||
kb_id?: string;
|
||||
list?: DomainShareNodeListItemResp[];
|
||||
meta?: DomainNodeMeta;
|
||||
name?: string;
|
||||
parent_id?: string;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue