Compare commits

...

7 Commits

Author SHA1 Message Date
Coltea 79234c2394
Merge pull request #1547 from jiangwel/feat-mcp
feat: mcp server
2025-11-24 20:40:36 +08:00
jiangwel 67e796dbb7 feat(chat): 添加 ChatRagOnlyRequset 结构并重构 ChatRagOnly 逻辑
重构 ChatRagOnly 方法,使用新的 ChatRagOnlyRequset 结构作为参数
简化聊天逻辑,直接获取并返回相关文档内容
移除不必要的会话管理代码
2025-11-24 20:21:17 +08:00
Coltea 55c563cc48
Merge pull request #1554 from KuaiYu95/fix/key
fix: 修复ctrlKey+b 富文本模式下加粗同时收起展示目录的问题
2025-11-24 19:02:33 +08:00
yu.kuai cfdc546d20 fix: 修复ctrlKey+b 富文本模式下加粗同时收起展示目录的问题 2025-11-24 18:56:45 +08:00
Coltea 7f58308dae
Merge pull request #1553 from KuaiYu95/fe/editor-update
表格功能丰富,优化编辑器体验
2025-11-24 18:25:13 +08:00
yu.kuai f34864621a feat: 表格功能丰富
----
1. hover 展示 table handle,点击可对表格的行/列进行操作
2. hover 展示 insert button,点击可相对当前行左右插入行,当前列左右插入列
3. 多选选中单元格支持对选中单元格设置
4. hover 最后一行/列展示 insert handle,点击可最后一行/列后面插入行/列
----

fix: 修复了编辑器的一些问题
----
1. 修复了编辑模式图片不能预览的问题
2. 修复了设置文字大小后,行高未能自动变化的问题
3. 修复了创建标题,输入拼音被打断 IME 问题
4. 修复了编辑页面编辑器聚焦时 cmd+s 保存无反应
5. 修复了空格缩进导致代码块不展示的问题
6. 视频支持设置自适应宽度,四角拖拽改变宽高,支持水平对齐设置
----
2025-11-24 18:14:14 +08:00
jiangwel da9039ff37 feat: mcp server 2025-11-24 15:48:47 +08:00
23 changed files with 1027 additions and 411 deletions

View File

@ -24,6 +24,7 @@ const (
SourceTypeDiscordBot SourceType = "discord_bot"
SourceTypeWechatOfficialAccount SourceType = "wechat_official_account"
SourceTypeOpenAIAPI SourceType = "openai_api"
SourceTypeMcpServer SourceType = "mcp_server"
)
func (s SourceType) Name() string {
@ -46,6 +47,8 @@ func (s SourceType) Name() string {
return "Discord 机器人"
case SourceTypeWechatOfficialAccount:
return "微信公众号"
case SourceTypeMcpServer:
return "MCP 服务器"
default:
return ""
}

View File

@ -240,7 +240,8 @@ const docTemplate = `{
"wechat_service_bot",
"discord_bot",
"wechat_official_account",
"openai_api"
"openai_api",
"mcp_server"
],
"type": "string",
"x-enum-varnames": [
@ -260,7 +261,8 @@ const docTemplate = `{
"SourceTypeWechatServiceBot",
"SourceTypeDiscordBot",
"SourceTypeWechatOfficialAccount",
"SourceTypeOpenAIAPI"
"SourceTypeOpenAIAPI",
"SourceTypeMcpServer"
],
"name": "source_type",
"in": "query",
@ -4268,7 +4270,8 @@ const docTemplate = `{
"wechat_service_bot",
"discord_bot",
"wechat_official_account",
"openai_api"
"openai_api",
"mcp_server"
],
"x-enum-varnames": [
"SourceTypeDingTalk",
@ -4287,7 +4290,8 @@ const docTemplate = `{
"SourceTypeWechatServiceBot",
"SourceTypeDiscordBot",
"SourceTypeWechatOfficialAccount",
"SourceTypeOpenAIAPI"
"SourceTypeOpenAIAPI",
"SourceTypeMcpServer"
]
},
"consts.StatDay": {
@ -4623,6 +4627,14 @@ const docTemplate = `{
}
]
},
"mcp_server_settings": {
"description": "MCP Server Settings",
"allOf": [
{
"$ref": "#/definitions/domain.MCPServerSettings"
}
]
},
"openai_api_bot_settings": {
"description": "OpenAI API Bot settings",
"allOf": [
@ -4896,6 +4908,14 @@ const docTemplate = `{
}
]
},
"mcp_server_settings": {
"description": "MCP Server Settings",
"allOf": [
{
"$ref": "#/definitions/domain.MCPServerSettings"
}
]
},
"openai_api_bot_settings": {
"description": "OpenAI API settings",
"allOf": [
@ -5056,7 +5076,8 @@ const docTemplate = `{
8,
9,
10,
11
11,
12
],
"x-enum-varnames": [
"AppTypeWeb",
@ -5069,7 +5090,8 @@ const docTemplate = `{
"AppTypeWechatOfficialAccount",
"AppTypeOpenAIAPI",
"AppTypeWecomAIBot",
"AppTypeLarkBot"
"AppTypeLarkBot",
"AppTypeMcpServer"
]
},
"domain.AuthUserInfo": {
@ -6378,6 +6400,31 @@ const docTemplate = `{
}
}
},
"domain.MCPServerSettings": {
"type": "object",
"properties": {
"docs_tool_settings": {
"$ref": "#/definitions/domain.MCPToolSettings"
},
"is_enabled": {
"type": "boolean"
},
"sample_auth": {
"$ref": "#/definitions/domain.SimpleAuth"
}
}
},
"domain.MCPToolSettings": {
"type": "object",
"properties": {
"desc": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"domain.MessageContent": {
"type": "object"
},

View File

@ -233,7 +233,8 @@
"wechat_service_bot",
"discord_bot",
"wechat_official_account",
"openai_api"
"openai_api",
"mcp_server"
],
"type": "string",
"x-enum-varnames": [
@ -253,7 +254,8 @@
"SourceTypeWechatServiceBot",
"SourceTypeDiscordBot",
"SourceTypeWechatOfficialAccount",
"SourceTypeOpenAIAPI"
"SourceTypeOpenAIAPI",
"SourceTypeMcpServer"
],
"name": "source_type",
"in": "query",
@ -4261,7 +4263,8 @@
"wechat_service_bot",
"discord_bot",
"wechat_official_account",
"openai_api"
"openai_api",
"mcp_server"
],
"x-enum-varnames": [
"SourceTypeDingTalk",
@ -4280,7 +4283,8 @@
"SourceTypeWechatServiceBot",
"SourceTypeDiscordBot",
"SourceTypeWechatOfficialAccount",
"SourceTypeOpenAIAPI"
"SourceTypeOpenAIAPI",
"SourceTypeMcpServer"
]
},
"consts.StatDay": {
@ -4616,6 +4620,14 @@
}
]
},
"mcp_server_settings": {
"description": "MCP Server Settings",
"allOf": [
{
"$ref": "#/definitions/domain.MCPServerSettings"
}
]
},
"openai_api_bot_settings": {
"description": "OpenAI API Bot settings",
"allOf": [
@ -4889,6 +4901,14 @@
}
]
},
"mcp_server_settings": {
"description": "MCP Server Settings",
"allOf": [
{
"$ref": "#/definitions/domain.MCPServerSettings"
}
]
},
"openai_api_bot_settings": {
"description": "OpenAI API settings",
"allOf": [
@ -5049,7 +5069,8 @@
8,
9,
10,
11
11,
12
],
"x-enum-varnames": [
"AppTypeWeb",
@ -5062,7 +5083,8 @@
"AppTypeWechatOfficialAccount",
"AppTypeOpenAIAPI",
"AppTypeWecomAIBot",
"AppTypeLarkBot"
"AppTypeLarkBot",
"AppTypeMcpServer"
]
},
"domain.AuthUserInfo": {
@ -6371,6 +6393,31 @@
}
}
},
"domain.MCPServerSettings": {
"type": "object",
"properties": {
"docs_tool_settings": {
"$ref": "#/definitions/domain.MCPToolSettings"
},
"is_enabled": {
"type": "boolean"
},
"sample_auth": {
"$ref": "#/definitions/domain.SimpleAuth"
}
}
},
"domain.MCPToolSettings": {
"type": "object",
"properties": {
"desc": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"domain.MessageContent": {
"type": "object"
},

View File

@ -245,6 +245,7 @@ definitions:
- discord_bot
- wechat_official_account
- openai_api
- mcp_server
type: string
x-enum-varnames:
- SourceTypeDingTalk
@ -264,6 +265,7 @@ definitions:
- SourceTypeDiscordBot
- SourceTypeWechatOfficialAccount
- SourceTypeOpenAIAPI
- SourceTypeMcpServer
consts.StatDay:
enum:
- 1
@ -488,6 +490,10 @@ definitions:
allOf:
- $ref: '#/definitions/domain.LarkBotSettings'
description: LarkBot
mcp_server_settings:
allOf:
- $ref: '#/definitions/domain.MCPServerSettings'
description: MCP Server Settings
openai_api_bot_settings:
allOf:
- $ref: '#/definitions/domain.OpenAIAPIBotSettings'
@ -660,6 +666,10 @@ definitions:
allOf:
- $ref: '#/definitions/domain.LarkBotSettings'
description: LarkBot
mcp_server_settings:
allOf:
- $ref: '#/definitions/domain.MCPServerSettings'
description: MCP Server Settings
openai_api_bot_settings:
allOf:
- $ref: '#/definitions/domain.OpenAIAPIBotSettings'
@ -767,6 +777,7 @@ definitions:
- 9
- 10
- 11
- 12
format: int32
type: integer
x-enum-varnames:
@ -781,6 +792,7 @@ definitions:
- AppTypeOpenAIAPI
- AppTypeWecomAIBot
- AppTypeLarkBot
- AppTypeMcpServer
domain.AuthUserInfo:
properties:
avatar_url:
@ -1625,6 +1637,22 @@ definitions:
url:
type: string
type: object
domain.MCPServerSettings:
properties:
docs_tool_settings:
$ref: '#/definitions/domain.MCPToolSettings'
is_enabled:
type: boolean
sample_auth:
$ref: '#/definitions/domain.SimpleAuth'
type: object
domain.MCPToolSettings:
properties:
desc:
type: string
name:
type: string
type: object
domain.MessageContent:
type: object
domain.MessageFrom:
@ -3359,6 +3387,7 @@ paths:
- discord_bot
- wechat_official_account
- openai_api
- mcp_server
in: query
name: source_type
required: true
@ -3381,6 +3410,7 @@ paths:
- SourceTypeDiscordBot
- SourceTypeWechatOfficialAccount
- SourceTypeOpenAIAPI
- SourceTypeMcpServer
produces:
- application/json
responses:

View File

@ -24,6 +24,7 @@ const (
AppTypeOpenAIAPI
AppTypeWecomAIBot
AppTypeLarkBot
AppTypeMcpServer
)
var AppTypes = []AppType{
@ -38,6 +39,7 @@ var AppTypes = []AppType{
AppTypeOpenAIAPI,
AppTypeWecomAIBot,
AppTypeLarkBot,
AppTypeMcpServer,
}
func (t AppType) ToSourceType() consts.SourceType {
@ -64,6 +66,8 @@ func (t AppType) ToSourceType() consts.SourceType {
return consts.SourceTypeOpenAIAPI
case AppTypeLarkBot:
return consts.SourceTypeLarkBot
case AppTypeMcpServer:
return consts.SourceTypeMcpServer
default:
return ""
}
@ -167,6 +171,8 @@ type AppSettings struct {
ContributeSettings ContributeSettings `json:"contribute_settings"`
HomePageSetting consts.HomePageSetting `json:"home_page_setting"`
ConversationSetting ConversationSetting `json:"conversation_setting"`
// MCP Server Settings
MCPServerSettings MCPServerSettings `json:"mcp_server_settings,omitempty"`
}
type ConversationSetting struct {
@ -178,6 +184,17 @@ type WebAppLandingTheme struct {
Name string `json:"name"`
}
type MCPServerSettings struct {
IsEnabled bool `json:"is_enabled"`
DocsToolSettings MCPToolSettings `json:"docs_tool_settings"`
SampleAuth SimpleAuth `json:"sample_auth"`
}
type MCPToolSettings struct {
Name string `json:"name"`
Desc string `json:"desc"`
}
type LarkBotSettings struct {
IsEnabled *bool `json:"is_enabled"`
AppID string `json:"app_id"`
@ -544,6 +561,8 @@ type AppSettingsResp struct {
WebAppLandingTheme WebAppLandingTheme `json:"web_app_landing_theme"`
HomePageSetting consts.HomePageSetting `json:"home_page_setting"`
ConversationSetting ConversationSetting `json:"conversation_setting"`
// MCP Server Settings
MCPServerSettings MCPServerSettings `json:"mcp_server_settings,omitempty"`
}
type WebAppLandingConfigResp struct {

View File

@ -23,6 +23,15 @@ type ChatRequest struct {
Info ConversationInfo `json:"-"`
}
type ChatRagOnlyRequest struct {
Message string `json:"message" validate:"required"`
KBID string `json:"-" validate:"required"`
UserInfo UserInfo `json:"user_info"`
AppType AppType `json:"app_type" validate:"required,oneof=1 2"`
}
type ConversationInfo struct {
UserInfo UserInfo `json:"user_info"`
}

View File

@ -19,6 +19,7 @@ type BaseEditionLimitation struct {
AllowWatermark bool `json:"allow_watermark"` // 支持水印
AllowCopyProtection bool `json:"allow_copy_protection"` // 支持内容复制保护
AllowOpenAIBotSettings bool `json:"allow_open_ai_bot_settings"` // 支持问答机器人
AllowMCPServer bool `json:"allow_mcp_server"` // 支持创建MCP Server
}
var baseEditionLimitationDefault = BaseEditionLimitation{

View File

@ -35,6 +35,7 @@ require (
github.com/larksuite/oapi-sdk-go/v3 v3.4.20
github.com/lib/pq v1.10.9
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20250508043914-ed57fa5c5274
github.com/mark3labs/mcp-go v0.43.0
github.com/microcosm-cc/bluemonday v1.0.27
github.com/mileusna/useragent v1.3.5
github.com/minio/minio-go/v7 v7.0.91
@ -137,6 +138,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@ -186,6 +188,7 @@ require (
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect

View File

@ -321,6 +321,8 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@ -396,6 +398,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.43.0 h1:lgiKcWMddh4sngbU+hoWOZ9iAe/qp/m851RQpj3Y7jA=
github.com/mark3labs/mcp-go v0.43.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -576,6 +580,8 @@ github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJ
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

@ -1 +1 @@
Subproject commit f6497a225d4b6ad76fe2dbedb427bd0e464a7662
Subproject commit 7771d31053770721d75e194d1c6e7a4b60fb58aa

View File

@ -2,5 +2,7 @@ package backend
import (
_ "github.com/jinzhu/copier"
_ "github.com/mark3labs/mcp-go/mcp"
_ "github.com/mark3labs/mcp-go/server"
_ "google.golang.org/protobuf/types/known/emptypb"
)

View File

@ -169,7 +169,7 @@ func (r *KnowledgeBaseRepository) SyncKBAccessSettingsToCaddy(ctx context.Contex
{
"match": []map[string]any{
{
"path": []string{"/share/v1/chat/completions", "/share/v1/app/wechat/app", "/share/v1/app/wechat/service", "/sitemap.xml", "/share/v1/app/wechat/official_account", "/share/v1/app/wechat/service/answer"},
"path": []string{"/share/v1/chat/completions", "/share/v1/app/wechat/app", "/share/v1/app/wechat/service", "/sitemap.xml", "/share/v1/app/wechat/official_account", "/share/v1/app/wechat/service/answer", "/mcp"},
},
},
"handle": []map[string]any{

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS mcp_calls;

View File

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS mcp_calls (
id SERIAL PRIMARY KEY,
mcp_session_id TEXT NOT NULL,
kb_id TEXT NOT NULL,
remote_ip TEXT,
initialize_req JSONB,
initialize_resp JSONB,
tool_call_req JSONB,
tool_call_resp TEXT,
created_at timestamptz NOT NULL DEFAULT NOW()
);

View File

@ -133,6 +133,12 @@ func (u *AppUsecase) ValidateUpdateApp(ctx context.Context, id string, req *doma
}
}
if !limitation.AllowMCPServer {
if app.Settings.MCPServerSettings.IsEnabled != req.Settings.MCPServerSettings.IsEnabled {
return domain.ErrPermissionDenied
}
}
return nil
}
@ -527,6 +533,8 @@ func (u *AppUsecase) GetAppDetailByKBIDAndAppType(ctx context.Context, kbID stri
ConversationSetting: app.Settings.ConversationSetting,
WecomAIBotSettings: app.Settings.WecomAIBotSettings,
MCPServerSettings: app.Settings.MCPServerSettings,
}
if !domain.GetBaseEditionLimitation(ctx).AllowCustomCopyright {
@ -556,6 +564,19 @@ func (u *AppUsecase) GetAppDetailByKBIDAndAppType(ctx context.Context, kbID stri
return appDetailResp, nil
}
func (u *AppUsecase) GetMCPServerAppInfo(ctx context.Context, kbID string) (*domain.AppInfoResp, error) {
apiApp, err := u.repo.GetOrCreateAppByKBIDAndType(ctx, kbID, domain.AppTypeMcpServer)
if err != nil {
return nil, err
}
appInfo := &domain.AppInfoResp{
Settings: domain.AppSettingsResp{
MCPServerSettings: apiApp.Settings.MCPServerSettings,
},
}
return appInfo, nil
}
func (u *AppUsecase) ShareGetWebAppInfo(ctx context.Context, kbID string, authId uint) (*domain.AppInfoResp, error) {
app, err := u.repo.GetOrCreateAppByKBIDAndType(ctx, kbID, domain.AppTypeWeb)
if err != nil {

View File

@ -301,6 +301,64 @@ func (u *ChatUsecase) Chat(ctx context.Context, req *domain.ChatRequest) (<-chan
return eventCh, nil
}
func (u *ChatUsecase) ChatRagOnly(ctx context.Context, req *domain.ChatRagOnlyRequest) (<-chan domain.SSEEvent, error) {
eventCh := make(chan domain.SSEEvent, 100)
go func() {
defer close(eventCh)
// extra1. if user set question block words then check it
blockWords, err := u.blockWordRepo.GetBlockWords(ctx, req.KBID)
if err != nil {
u.logger.Error("failed to get question block words", log.Error(err))
eventCh <- domain.SSEEvent{Type: "error", Content: "failed to get question block words"}
return
}
if len(blockWords) > 0 { // check --> filter
questionFilter := utils.GetDFA(req.KBID)
if err := questionFilter.DFA.Check(req.Message); err != nil { // exist then return err
answer := "**您的问题包含敏感词, AI 无法回答您的问题。**"
eventCh <- domain.SSEEvent{Type: "error", Content: answer}
return
}
}
if req.UserInfo.AuthUserID == 0 {
auth, _ := u.AuthRepo.GetAuthBySourceType(ctx, req.AppType.ToSourceType())
if auth != nil {
req.UserInfo.AuthUserID = auth.ID
}
}
groupIds, err := u.AuthRepo.GetAuthGroupIdsWithParentsByAuthId(ctx, req.UserInfo.AuthUserID)
if err != nil {
u.logger.Error("failed to get auth groupIds", log.Error(err))
eventCh <- domain.SSEEvent{Type: "error", Content: "failed to get auth groupIds"}
return
}
// retrieve documents
kb, err := u.kbRepo.GetKnowledgeBaseByID(ctx, req.KBID)
if err != nil {
u.logger.Error("failed to get kb", log.Error(err))
eventCh <- domain.SSEEvent{Type: "error", Content: "failed to get kb"}
return
}
rankedNodes, err := u.llmUsecase.GetRankNodes(ctx, []string{kb.DatasetID}, req.Message, groupIds, 0, nil)
if err != nil {
u.logger.Error("failed to get rank nodes", log.Error(err))
eventCh <- domain.SSEEvent{Type: "error", Content: "failed to get rank nodes"}
return
}
documents := domain.FormatNodeChunks(rankedNodes, kb.AccessSettings.BaseURL)
u.logger.Debug("documents", log.String("documents", documents))
// send only the documents part
eventCh <- domain.SSEEvent{Type: "data", Content: documents}
eventCh <- domain.SSEEvent{Type: "done"}
}()
return eventCh, nil
}
func (u *ChatUsecase) CreateAcOnChunk(ctx context.Context, kbID string, answer *string, eventCh chan<- domain.SSEEvent, blockWords []string) (func(ctx context.Context, dataType, chunk string) error,
func(ctx context.Context, dataType string)) {
var buffer strings.Builder

View File

@ -2,13 +2,18 @@ package utils
import (
"net"
"net/http"
"strings"
"github.com/labstack/echo/v4"
)
func GetClientIPFromRemoteAddr(c echo.Context) string {
addr := c.Request().RemoteAddr
return ExtractHostFromRemoteAddr(c.Request())
}
func ExtractHostFromRemoteAddr(r *http.Request) string {
addr := r.RemoteAddr
if addr == "" {
return ""
}

View File

@ -2,6 +2,7 @@ import { copyText } from '@/utils';
import { Box, Stack } from '@mui/material';
import { Ellipsis } from '@ctzhian/ui';
import { IconFuzhi } from '@panda-wiki/icons';
import { message } from '@ctzhian/ui';
interface ShowTextProps {
text: string[];
@ -10,6 +11,7 @@ interface ShowTextProps {
noEllipsis?: boolean;
icon?: React.ReactNode;
onClick?: () => void;
forceCopy?: boolean;
}
const ShowText = ({
@ -21,6 +23,7 @@ const ShowText = ({
),
onClick,
noEllipsis = false,
forceCopy = false,
}: ShowTextProps) => {
return (
<Stack
@ -48,8 +51,32 @@ const ShowText = ({
onClick={
copyable
? () => {
copyText(text.join('\n'));
const content = text.join('\n');
if (forceCopy) {
try {
if (navigator.clipboard) {
navigator.clipboard.writeText(content);
message.success('复制成功');
} else {
const ta = document.createElement('textarea');
ta.style.position = 'fixed';
ta.style.opacity = '0';
ta.style.left = '-9999px';
ta.style.top = '-9999px';
ta.value = content;
document.body.appendChild(ta);
ta.focus();
ta.select();
const ok = document.execCommand('copy');
if (ok) message.success('复制成功');
document.body.removeChild(ta);
}
} catch (e) {}
onClick?.();
} else {
copyText(content);
onClick?.();
}
}
: onClick
}

View File

@ -1,5 +1,6 @@
import { uploadFile } from '@/api';
import Emoji from '@/components/Emoji';
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
import { postApiV1CreationTabComplete, putApiV1NodeDetail } from '@/request';
import { V1NodeDetailResp } from '@/request/types';
import { useAppSelector } from '@/store';
@ -13,6 +14,12 @@ import {
} from '@ctzhian/tiptap';
import { message } from '@ctzhian/ui';
import { Box, Stack, TextField, Tooltip } from '@mui/material';
import {
IconAShijian2,
IconDJzhinengzhaiyao,
IconTianjiawendang,
IconZiti,
} from '@panda-wiki/icons';
import dayjs from 'dayjs';
import { debounce } from 'lodash-es';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -29,13 +36,6 @@ import Header from './Header';
import Summary from './Summary';
import Toc from './Toc';
import Toolbar from './Toolbar';
import { BUSINESS_VERSION_PERMISSION } from '@/constant/version';
import {
IconTianjiawendang,
IconAShijian2,
IconZiti,
IconDJzhinengzhaiyao,
} from '@panda-wiki/icons';
interface WrapProps {
detail: V1NodeDetailResp;
@ -672,7 +672,19 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
</Box>
<Box
sx={{ ...(fixedToc && { display: 'flex' }) }}
onKeyDown={event => event.stopPropagation()}
onKeyDown={event => {
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
return;
}
if (
isMarkdown &&
(event.ctrlKey || event.metaKey) &&
event.key === 'b'
) {
return;
}
event.stopPropagation();
}}
>
{isMarkdown ? (
<Box

View File

@ -0,0 +1,272 @@
import { DomainKnowledgeBaseDetail } from '@/request/types';
import {
Box,
FormControl,
FormControlLabel,
Radio,
RadioGroup,
TextField,
Stack,
} from '@mui/material';
import { SettingCardItem, FormItem } from './Common';
import ShowText from '@/components/ShowText';
import { Controller, useForm } from 'react-hook-form';
import { useMemo, useState, useEffect } from 'react';
import { message } from '@ctzhian/ui';
import { getApiV1AppDetail, putApiV1App } from '@/request/App';
import { DomainAppDetailResp, ConstsLicenseEdition } from '@/request/types';
interface CardMCPProps {
kb: DomainKnowledgeBaseDetail;
}
const CardMCP = ({ kb }: CardMCPProps) => {
const [isEdit, setIsEdit] = useState(false);
const {
control,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm({
defaultValues: {
is_enabled: false,
access: 'open' as 'open' | 'auth',
token: '',
tool_name: '',
tool_desc: '',
},
});
const isEnabled = watch('is_enabled');
const access = watch('access');
const [detail, setDetail] = useState<DomainAppDetailResp | null>(null);
const mcpUrl = useMemo(() => {
const hostRaw = kb?.access_settings?.hosts?.[0] || window.location.hostname;
const host = hostRaw === '*' ? window.location.hostname : hostRaw;
const sslPorts = kb?.access_settings?.ssl_ports || [];
const httpPorts = kb?.access_settings?.ports || [];
const isHttps = sslPorts.length > 0;
const protocol = isHttps ? 'https' : 'http';
if (!host) {
return `${protocol}://${window.location.hostname}${isHttps ? '' : `:${window.location.port}`}/mcp`;
}
if (isHttps) {
return `${protocol}://${host}/mcp`;
}
const port = httpPorts[0];
if (!port) return `${protocol}://${host}/mcp`;
return `${protocol}://${host}:${port}/mcp`;
}, [kb]);
const onSubmit = handleSubmit(() => {
if (!kb || !detail) return;
const payload: any = {
kb_id: kb.id!,
settings: {
mcp_server_settings: {
is_enabled: isEnabled,
docs_tool_settings: {
name: watch('tool_name'),
desc: watch('tool_desc'),
},
sample_auth: {
enabled: access === 'auth',
password: access === 'auth' ? watch('token') : '',
},
},
},
};
putApiV1App({ id: detail.id! }, payload).then(() => {
message.success('保存成功');
setIsEdit(false);
getDetail();
});
});
const getDetail = () => {
getApiV1AppDetail({ kb_id: kb.id!, type: '12' }).then(res => {
setDetail(res);
const is_enabled =
(res.settings as any)?.mcp_server_settings?.is_enabled ?? false;
const auth =
(res.settings as any)?.mcp_server_settings?.sample_auth ?? {};
const accessVal = auth.enabled ? 'auth' : 'open';
const tokenVal = auth.password ?? '';
const toolName =
(res.settings as any)?.mcp_server_settings?.docs_tool_settings?.name ??
'';
const toolDesc =
(res.settings as any)?.mcp_server_settings?.docs_tool_settings?.desc ??
'';
setValue('is_enabled', is_enabled);
setValue('access', accessVal);
setValue('token', tokenVal);
setValue('tool_name', toolName);
setValue('tool_desc', toolDesc);
});
};
useEffect(() => {
if (!kb) return;
getDetail();
}, [kb]);
return (
<Box sx={{ width: 1000, margin: 'auto', pb: 4 }}>
<SettingCardItem
title='MCP 设置'
isEdit={isEdit}
onSubmit={onSubmit}
permission={[
ConstsLicenseEdition.LicenseEditionBusiness,
ConstsLicenseEdition.LicenseEditionEnterprise,
]}
more={{
type: 'link',
href: 'https://pandawiki.docs.baizhi.cloud/node/019aa45c-90c1-7e6f-b17a-74ab1b200153',
target: '_blank',
text: '使用方法',
}}
>
<FormItem label='MCP Server'>
<FormControl>
<Controller
control={control}
name='is_enabled'
render={({ field }) => (
<RadioGroup
{...field}
onChange={e => {
field.onChange(e.target.value === 'true');
setIsEdit(true);
}}
>
<Stack direction={'row'}>
<FormControlLabel
value={true}
control={<Radio size='small' />}
label={<Box sx={{ width: 100 }}></Box>}
/>
<FormControlLabel
value={false}
control={<Radio size='small' />}
label={<Box sx={{ width: 100 }}></Box>}
/>
</Stack>
</RadioGroup>
)}
/>
</FormControl>
</FormItem>
{isEnabled && (
<>
<FormItem label='MCP URL'>
<ShowText
text={[mcpUrl]}
copyable={true}
noEllipsis={true}
forceCopy={true}
/>
</FormItem>
<FormItem label='MCP Tool名称'>
<Controller
control={control}
name='tool_name'
render={({ field }) => (
<TextField
{...field}
fullWidth
placeholder='自定义检索文档MCP Tool名称'
onChange={e => {
field.onChange(e.target.value);
setIsEdit(true);
}}
/>
)}
/>
</FormItem>
<FormItem label='MCP Tool描述'>
<Controller
control={control}
name='tool_desc'
render={({ field }) => (
<TextField
{...field}
fullWidth
placeholder='自定义检索文档MCP Tool描述'
onChange={e => {
field.onChange(e.target.value);
setIsEdit(true);
}}
/>
)}
/>
</FormItem>
<FormItem label='访问控制'>
<FormControl>
<Controller
control={control}
name='access'
render={({ field }) => (
<RadioGroup
{...field}
onChange={e => {
field.onChange(e.target.value);
setIsEdit(true);
}}
>
<Stack direction={'row'}>
<FormControlLabel
value={'open'}
control={<Radio size='small' />}
label={<Box sx={{ width: 100 }}></Box>}
/>
<FormControlLabel
value={'auth'}
control={<Radio size='small' />}
label={<Box sx={{ width: 100 }}></Box>}
/>
</Stack>
</RadioGroup>
)}
/>
</FormControl>
</FormItem>
{access === 'auth' && (
<FormItem label='访问口令' required>
<Controller
control={control}
name='token'
rules={{ required: '访问口令不能为空' }}
render={({ field }) => (
<TextField
{...field}
fullWidth
placeholder='访问口令'
onChange={e => {
field.onChange(e.target.value);
setIsEdit(true);
}}
error={!!errors.token}
helperText={errors.token?.message}
/>
)}
/>
</FormItem>
)}
</>
)}
</SettingCardItem>
</Box>
);
};
export default CardMCP;

View File

@ -15,6 +15,7 @@ import CardKB from './component/CardKB';
import CardRobot from './component/CardRobot';
import CardSecurity from './component/CardSecurity';
import CardWeb from './component/CardWeb';
import CardMCP from './component/CardMCP';
const SettingTabs: { label: string; id: string }[] = [
{ label: '门户网站', id: 'portal-website' },
@ -23,6 +24,7 @@ const SettingTabs: { label: string; id: string }[] = [
{ label: '反馈设置', id: 'feedback' },
{ label: '安全设置', id: 'security' },
{ label: '访问控制', id: 'backend-info' },
{ label: 'MCP 设置', id: 'mcp' },
];
const Setting = () => {
@ -116,6 +118,7 @@ const Setting = () => {
{activeTab === 'feedback' && <CardFeedback kb={kb} />}
{activeTab === 'robot' && <CardRobot kb={kb} url={url} />}
{activeTab === 'portal-website' && <CardWeb kb={kb} refresh={getKb} />}
{activeTab === 'mcp' && <CardMCP kb={kb} />}
</Card>
</Box>
);

View File

@ -18,7 +18,7 @@
"license": "ISC",
"packageManager": "pnpm@10.12.1",
"dependencies": {
"@ctzhian/tiptap": "^1.13.4",
"@ctzhian/tiptap": "^2.1.2",
"@ctzhian/ui": "^7.0.5",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",

File diff suppressed because it is too large Load Diff