Compare commits

..

No commits in common. "ca323de9b239cc24bc75fa725b0f67924494ca8f" and "a5c99fca95e9690e6b96c8baae1af389c8d6d9c4" have entirely different histories.

45 changed files with 1515 additions and 3015 deletions

View File

@ -96,9 +96,7 @@ func createApp() (*App, error) {
return nil, err return nil, err
} }
authRepo := pg2.NewAuthRepo(db, logger, cacheCache) authRepo := pg2.NewAuthRepo(db, logger, cacheCache)
systemSettingRepo := pg2.NewSystemSettingRepo(db, logger) nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo)
modelUsecase := usecase.NewModelUsecase(modelRepository, nodeRepository, ragRepository, ragService, logger, configConfig, knowledgeBaseRepository, systemSettingRepo)
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo, modelUsecase)
nodeHandler := v1.NewNodeHandler(baseHandler, echo, nodeUsecase, authMiddleware, logger) nodeHandler := v1.NewNodeHandler(baseHandler, echo, nodeUsecase, authMiddleware, logger)
geoRepo := cache2.NewGeoCache(cacheCache, db, logger) geoRepo := cache2.NewGeoCache(cacheCache, db, logger)
ipdbIPDB, err := ipdb.NewIPDB(configConfig, logger) ipdbIPDB, err := ipdb.NewIPDB(configConfig, logger)
@ -107,6 +105,7 @@ func createApp() (*App, error) {
} }
ipAddressRepo := ipdb2.NewIPAddressRepo(ipdbIPDB, logger) ipAddressRepo := ipdb2.NewIPAddressRepo(ipdbIPDB, logger)
conversationUsecase := usecase.NewConversationUsecase(conversationRepository, nodeRepository, geoRepo, logger, ipAddressRepo, authRepo) conversationUsecase := usecase.NewConversationUsecase(conversationRepository, nodeRepository, geoRepo, logger, ipAddressRepo, authRepo)
modelUsecase := usecase.NewModelUsecase(modelRepository, nodeRepository, ragRepository, ragService, logger, configConfig, knowledgeBaseRepository)
blockWordRepo := pg2.NewBlockWordRepo(db, logger) blockWordRepo := pg2.NewBlockWordRepo(db, logger)
chatUsecase, err := usecase.NewChatUsecase(llmUsecase, knowledgeBaseRepository, conversationUsecase, modelUsecase, appRepository, blockWordRepo, authRepo, logger) chatUsecase, err := usecase.NewChatUsecase(llmUsecase, knowledgeBaseRepository, conversationUsecase, modelUsecase, appRepository, blockWordRepo, authRepo, logger)
if err != nil { if err != nil {

View File

@ -8,12 +8,12 @@ package main
import ( import (
"github.com/chaitin/panda-wiki/config" "github.com/chaitin/panda-wiki/config"
mq3 "github.com/chaitin/panda-wiki/handler/mq" mq2 "github.com/chaitin/panda-wiki/handler/mq"
"github.com/chaitin/panda-wiki/log" "github.com/chaitin/panda-wiki/log"
"github.com/chaitin/panda-wiki/mq" "github.com/chaitin/panda-wiki/mq"
cache2 "github.com/chaitin/panda-wiki/repo/cache" cache2 "github.com/chaitin/panda-wiki/repo/cache"
ipdb2 "github.com/chaitin/panda-wiki/repo/ipdb" ipdb2 "github.com/chaitin/panda-wiki/repo/ipdb"
mq2 "github.com/chaitin/panda-wiki/repo/mq" mq3 "github.com/chaitin/panda-wiki/repo/mq"
pg2 "github.com/chaitin/panda-wiki/repo/pg" pg2 "github.com/chaitin/panda-wiki/repo/pg"
"github.com/chaitin/panda-wiki/store/cache" "github.com/chaitin/panda-wiki/store/cache"
"github.com/chaitin/panda-wiki/store/ipdb" "github.com/chaitin/panda-wiki/store/ipdb"
@ -49,18 +49,11 @@ func createApp() (*App, error) {
modelRepository := pg2.NewModelRepository(db, logger) modelRepository := pg2.NewModelRepository(db, logger)
promptRepo := pg2.NewPromptRepo(db, logger) promptRepo := pg2.NewPromptRepo(db, logger)
llmUsecase := usecase.NewLLMUsecase(configConfig, ragService, conversationRepository, knowledgeBaseRepository, nodeRepository, modelRepository, promptRepo, logger) llmUsecase := usecase.NewLLMUsecase(configConfig, ragService, conversationRepository, knowledgeBaseRepository, nodeRepository, modelRepository, promptRepo, logger)
mqProducer, err := mq.NewMQProducer(configConfig, logger) ragmqHandler, err := mq2.NewRAGMQHandler(mqConsumer, logger, ragService, nodeRepository, knowledgeBaseRepository, llmUsecase, modelRepository)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ragRepository := mq2.NewRAGRepository(mqProducer) ragDocUpdateHandler, err := mq2.NewRagDocUpdateHandler(mqConsumer, logger, nodeRepository)
systemSettingRepo := pg2.NewSystemSettingRepo(db, logger)
modelUsecase := usecase.NewModelUsecase(modelRepository, nodeRepository, ragRepository, ragService, logger, configConfig, knowledgeBaseRepository, systemSettingRepo)
ragmqHandler, err := mq3.NewRAGMQHandler(mqConsumer, logger, ragService, nodeRepository, knowledgeBaseRepository, llmUsecase, modelUsecase)
if err != nil {
return nil, err
}
ragDocUpdateHandler, err := mq3.NewRagDocUpdateHandler(mqConsumer, logger, nodeRepository)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -78,17 +71,22 @@ func createApp() (*App, error) {
geoRepo := cache2.NewGeoCache(cacheCache, db, logger) geoRepo := cache2.NewGeoCache(cacheCache, db, logger)
authRepo := pg2.NewAuthRepo(db, logger, cacheCache) authRepo := pg2.NewAuthRepo(db, logger, cacheCache)
statUseCase := usecase.NewStatUseCase(statRepository, nodeRepository, conversationRepository, appRepository, ipAddressRepo, geoRepo, authRepo, knowledgeBaseRepository, logger) statUseCase := usecase.NewStatUseCase(statRepository, nodeRepository, conversationRepository, appRepository, ipAddressRepo, geoRepo, authRepo, knowledgeBaseRepository, logger)
mqProducer, err := mq.NewMQProducer(configConfig, logger)
if err != nil {
return nil, err
}
ragRepository := mq3.NewRAGRepository(mqProducer)
userRepository := pg2.NewUserRepository(db, logger) userRepository := pg2.NewUserRepository(db, logger)
minioClient, err := s3.NewMinioClient(configConfig) minioClient, err := s3.NewMinioClient(configConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo, modelUsecase) nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo)
cronHandler, err := mq3.NewStatCronHandler(logger, statRepository, statUseCase, nodeUsecase) cronHandler, err := mq2.NewStatCronHandler(logger, statRepository, statUseCase, nodeUsecase)
if err != nil { if err != nil {
return nil, err return nil, err
} }
mqHandlers := &mq3.MQHandlers{ mqHandlers := &mq2.MQHandlers{
RAGMQHandler: ragmqHandler, RAGMQHandler: ragmqHandler,
RagDocUpdateHandler: ragDocUpdateHandler, RagDocUpdateHandler: ragDocUpdateHandler,
StatCronHandler: cronHandler, StatCronHandler: cronHandler,
@ -107,6 +105,6 @@ func createApp() (*App, error) {
type App struct { type App struct {
MQConsumer mq.MQConsumer MQConsumer mq.MQConsumer
Config *config.Config Config *config.Config
MQHandlers *mq3.MQHandlers MQHandlers *mq2.MQHandlers
StatCronHandler *mq3.CronHandler StatCronHandler *mq2.CronHandler
} }

View File

@ -60,9 +60,7 @@ func createApp() (*App, error) {
return nil, err return nil, err
} }
authRepo := pg2.NewAuthRepo(db, logger, cacheCache) authRepo := pg2.NewAuthRepo(db, logger, cacheCache)
systemSettingRepo := pg2.NewSystemSettingRepo(db, logger) nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo)
modelUsecase := usecase.NewModelUsecase(modelRepository, nodeRepository, ragRepository, ragService, logger, configConfig, knowledgeBaseRepository, systemSettingRepo)
nodeUsecase := usecase.NewNodeUsecase(nodeRepository, appRepository, ragRepository, userRepository, knowledgeBaseRepository, llmUsecase, ragService, logger, minioClient, modelRepository, authRepo, modelUsecase)
kbRepo := cache2.NewKBRepo(cacheCache) kbRepo := cache2.NewKBRepo(cacheCache)
knowledgeBaseUsecase, err := usecase.NewKnowledgeBaseUsecase(knowledgeBaseRepository, nodeRepository, ragRepository, userRepository, ragService, kbRepo, logger, configConfig) knowledgeBaseUsecase, err := usecase.NewKnowledgeBaseUsecase(knowledgeBaseRepository, nodeRepository, ragRepository, userRepository, ragService, kbRepo, logger, configConfig)
if err != nil { if err != nil {

View File

@ -1,39 +0,0 @@
package consts
type AutoModeDefaultModel string
const (
AutoModeDefaultChatModel AutoModeDefaultModel = "deepseek-chat"
AutoModeDefaultEmbeddingModel AutoModeDefaultModel = "bge-m3"
AutoModeDefaultRerankModel AutoModeDefaultModel = "bge-reranker-v2-m3"
AutoModeDefaultAnalysisModel AutoModeDefaultModel = "qwen2.5-3b-instruct"
AutoModeDefaultAnalysisVLModel AutoModeDefaultModel = "qwen3-vl-max"
)
func GetAutoModeDefaultModel(modelType string) string {
switch modelType {
case "chat":
return string(AutoModeDefaultChatModel)
case "embedding":
return string(AutoModeDefaultEmbeddingModel)
case "rerank":
return string(AutoModeDefaultRerankModel)
case "analysis":
return string(AutoModeDefaultAnalysisModel)
case "analysis-vl":
return string(AutoModeDefaultAnalysisVLModel)
default:
return string(AutoModeDefaultChatModel)
}
}
type ModelSettingMode string
const (
ModelSettingModeManual ModelSettingMode = "manual"
ModelSettingModeAuto ModelSettingMode = "auto"
)
const (
AutoModeBaseURL = "https://model-square.app.baizhi.cloud/v1"
)

View File

@ -1,7 +0,0 @@
package consts
type SystemSettingKey string
const (
SystemSettingModelMode SystemSettingKey = "model_setting_mode"
)

View File

@ -1565,41 +1565,6 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/model/mode-setting": {
"get": {
"description": "get current model mode setting including mode, API key and chat model",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"model"
],
"summary": "get model mode setting",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.ModelModeSetting"
}
}
}
]
}
}
}
}
},
"/api/v1/model/provider/supported": { "/api/v1/model/provider/supported": {
"post": { "post": {
"description": "get provider supported model list", "description": "get provider supported model list",
@ -1646,52 +1611,6 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/model/switch-mode": {
"post": {
"description": "switch model mode between manual and auto",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"model"
],
"summary": "switch mode",
"parameters": [
{
"description": "switch mode request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.SwitchModeReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.SwitchModeResp"
}
}
}
]
}
}
}
}
},
"/api/v1/node": { "/api/v1/node": {
"post": { "post": {
"security": [ "security": [
@ -4085,17 +4004,6 @@ const docTemplate = `{
"LicenseEditionEnterprise" "LicenseEditionEnterprise"
] ]
}, },
"consts.ModelSettingMode": {
"type": "string",
"enum": [
"manual",
"auto"
],
"x-enum-varnames": [
"ModelSettingModeManual",
"ModelSettingModeAuto"
]
},
"consts.NodeAccessPerm": { "consts.NodeAccessPerm": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -6350,31 +6258,6 @@ const docTemplate = `{
} }
} }
}, },
"domain.ModelModeSetting": {
"type": "object",
"properties": {
"auto_mode_api_key": {
"description": "百智云 API Key",
"type": "string"
},
"chat_model": {
"description": "自定义对话模型名称",
"type": "string"
},
"is_manual_embedding_updated": {
"description": "手动模式下嵌入模型是否更新",
"type": "boolean"
},
"mode": {
"description": "模式: manual 或 auto",
"allOf": [
{
"$ref": "#/definitions/consts.ModelSettingMode"
}
]
}
}
},
"domain.ModelType": { "domain.ModelType": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -7214,37 +7097,6 @@ const docTemplate = `{
"StatPageSceneLogin" "StatPageSceneLogin"
] ]
}, },
"domain.SwitchModeReq": {
"type": "object",
"required": [
"mode"
],
"properties": {
"auto_mode_api_key": {
"description": "百智云 API Key",
"type": "string"
},
"chat_model": {
"description": "自定义对话模型名称",
"type": "string"
},
"mode": {
"type": "string",
"enum": [
"manual",
"auto"
]
}
}
},
"domain.SwitchModeResp": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
},
"domain.TextConfig": { "domain.TextConfig": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -1558,41 +1558,6 @@
} }
} }
}, },
"/api/v1/model/mode-setting": {
"get": {
"description": "get current model mode setting including mode, API key and chat model",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"model"
],
"summary": "get model mode setting",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.ModelModeSetting"
}
}
}
]
}
}
}
}
},
"/api/v1/model/provider/supported": { "/api/v1/model/provider/supported": {
"post": { "post": {
"description": "get provider supported model list", "description": "get provider supported model list",
@ -1639,52 +1604,6 @@
} }
} }
}, },
"/api/v1/model/switch-mode": {
"post": {
"description": "switch model mode between manual and auto",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"model"
],
"summary": "switch mode",
"parameters": [
{
"description": "switch mode request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.SwitchModeReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/domain.SwitchModeResp"
}
}
}
]
}
}
}
}
},
"/api/v1/node": { "/api/v1/node": {
"post": { "post": {
"security": [ "security": [
@ -4078,17 +3997,6 @@
"LicenseEditionEnterprise" "LicenseEditionEnterprise"
] ]
}, },
"consts.ModelSettingMode": {
"type": "string",
"enum": [
"manual",
"auto"
],
"x-enum-varnames": [
"ModelSettingModeManual",
"ModelSettingModeAuto"
]
},
"consts.NodeAccessPerm": { "consts.NodeAccessPerm": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -6343,31 +6251,6 @@
} }
} }
}, },
"domain.ModelModeSetting": {
"type": "object",
"properties": {
"auto_mode_api_key": {
"description": "百智云 API Key",
"type": "string"
},
"chat_model": {
"description": "自定义对话模型名称",
"type": "string"
},
"is_manual_embedding_updated": {
"description": "手动模式下嵌入模型是否更新",
"type": "boolean"
},
"mode": {
"description": "模式: manual 或 auto",
"allOf": [
{
"$ref": "#/definitions/consts.ModelSettingMode"
}
]
}
}
},
"domain.ModelType": { "domain.ModelType": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -7207,37 +7090,6 @@
"StatPageSceneLogin" "StatPageSceneLogin"
] ]
}, },
"domain.SwitchModeReq": {
"type": "object",
"required": [
"mode"
],
"properties": {
"auto_mode_api_key": {
"description": "百智云 API Key",
"type": "string"
},
"chat_model": {
"description": "自定义对话模型名称",
"type": "string"
},
"mode": {
"type": "string",
"enum": [
"manual",
"auto"
]
}
}
},
"domain.SwitchModeResp": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
},
"domain.TextConfig": { "domain.TextConfig": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -131,14 +131,6 @@ definitions:
- LicenseEditionFree - LicenseEditionFree
- LicenseEditionContributor - LicenseEditionContributor
- LicenseEditionEnterprise - LicenseEditionEnterprise
consts.ModelSettingMode:
enum:
- manual
- auto
type: string
x-enum-varnames:
- ModelSettingModeManual
- ModelSettingModeAuto
consts.NodeAccessPerm: consts.NodeAccessPerm:
enum: enum:
- open - open
@ -1636,22 +1628,6 @@ definitions:
type: type:
type: string type: string
type: object type: object
domain.ModelModeSetting:
properties:
auto_mode_api_key:
description: 百智云 API Key
type: string
chat_model:
description: 自定义对话模型名称
type: string
is_manual_embedding_updated:
description: 手动模式下嵌入模型是否更新
type: boolean
mode:
allOf:
- $ref: '#/definitions/consts.ModelSettingMode'
description: '模式: manual 或 auto'
type: object
domain.ModelType: domain.ModelType:
enum: enum:
- chat - chat
@ -2202,27 +2178,6 @@ definitions:
- StatPageSceneNodeDetail - StatPageSceneNodeDetail
- StatPageSceneChat - StatPageSceneChat
- StatPageSceneLogin - StatPageSceneLogin
domain.SwitchModeReq:
properties:
auto_mode_api_key:
description: 百智云 API Key
type: string
chat_model:
description: 自定义对话模型名称
type: string
mode:
enum:
- manual
- auto
type: string
required:
- mode
type: object
domain.SwitchModeResp:
properties:
message:
type: string
type: object
domain.TextConfig: domain.TextConfig:
properties: properties:
title: title:
@ -4110,27 +4065,6 @@ paths:
summary: get model list summary: get model list
tags: tags:
- model - model
/api/v1/model/mode-setting:
get:
consumes:
- application/json
description: get current model mode setting including mode, API key and chat
model
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
$ref: '#/definitions/domain.ModelModeSetting'
type: object
summary: get model mode setting
tags:
- model
/api/v1/model/provider/supported: /api/v1/model/provider/supported:
post: post:
consumes: consumes:
@ -4158,33 +4092,6 @@ paths:
summary: get provider supported model list summary: get provider supported model list
tags: tags:
- model - model
/api/v1/model/switch-mode:
post:
consumes:
- application/json
description: switch model mode between manual and auto
parameters:
- description: switch mode request
in: body
name: request
required: true
schema:
$ref: '#/definitions/domain.SwitchModeReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
$ref: '#/definitions/domain.SwitchModeResp'
type: object
summary: switch mode
tags:
- model
/api/v1/node: /api/v1/node:
post: post:
consumes: consumes:

View File

@ -165,13 +165,3 @@ type ProviderModelListItem struct {
type ActivateModelReq struct { type ActivateModelReq struct {
ModelID string `json:"model_id" validate:"required"` ModelID string `json:"model_id" validate:"required"`
} }
type SwitchModeReq struct {
Mode string `json:"mode" validate:"required,oneof=manual auto"`
AutoModeAPIKey string `json:"auto_mode_api_key"` // 百智云 API Key
ChatModel string `json:"chat_model"` // 自定义对话模型名称
}
type SwitchModeResp struct {
Message string `json:"message"`
}

View File

@ -1,29 +0,0 @@
package domain
import (
"time"
"github.com/chaitin/panda-wiki/consts"
)
// table: settings
type SystemSetting struct {
ID int `json:"id" gorm:"primary_key"`
Key consts.SystemSettingKey `json:"key"`
Value []byte `json:"value" gorm:"type:jsonb"` // JSON string
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (SystemSetting) TableName() string {
return "system_settings"
}
// ModelModeSetting 模型配置结构体
type ModelModeSetting struct {
Mode consts.ModelSettingMode `json:"mode"` // 模式: manual 或 auto
AutoModeAPIKey string `json:"auto_mode_api_key"` // 百智云 API Key
ChatModel string `json:"chat_model"` // 自定义对话模型名称
IsManualEmbeddingUpdated bool `json:"is_manual_embedding_updated"` // 手动模式下嵌入模型是否更新
}

View File

@ -27,7 +27,6 @@ var ProviderSet = wire.NewSet(
usecase.NewLLMUsecase, usecase.NewLLMUsecase,
usecase.NewStatUseCase, usecase.NewStatUseCase,
usecase.NewNodeUsecase, usecase.NewNodeUsecase,
usecase.NewModelUsecase,
NewRAGMQHandler, NewRAGMQHandler,
NewRagDocUpdateHandler, NewRagDocUpdateHandler,

View File

@ -15,24 +15,24 @@ import (
) )
type RAGMQHandler struct { type RAGMQHandler struct {
consumer mq.MQConsumer consumer mq.MQConsumer
logger *log.Logger logger *log.Logger
rag rag.RAGService rag rag.RAGService
nodeRepo *pg.NodeRepository nodeRepo *pg.NodeRepository
kbRepo *pg.KnowledgeBaseRepository kbRepo *pg.KnowledgeBaseRepository
llmUsecase *usecase.LLMUsecase modelRepo *pg.ModelRepository
modelUsecase *usecase.ModelUsecase llmUsecase *usecase.LLMUsecase
} }
func NewRAGMQHandler(consumer mq.MQConsumer, logger *log.Logger, rag rag.RAGService, nodeRepo *pg.NodeRepository, kbRepo *pg.KnowledgeBaseRepository, llmUsecase *usecase.LLMUsecase, modelUsecase *usecase.ModelUsecase) (*RAGMQHandler, error) { func NewRAGMQHandler(consumer mq.MQConsumer, logger *log.Logger, rag rag.RAGService, nodeRepo *pg.NodeRepository, kbRepo *pg.KnowledgeBaseRepository, llmUsecase *usecase.LLMUsecase, modelRepo *pg.ModelRepository) (*RAGMQHandler, error) {
h := &RAGMQHandler{ h := &RAGMQHandler{
consumer: consumer, consumer: consumer,
logger: logger.WithModule("mq.rag"), logger: logger.WithModule("mq.rag"),
rag: rag, rag: rag,
nodeRepo: nodeRepo, nodeRepo: nodeRepo,
kbRepo: kbRepo, kbRepo: kbRepo,
llmUsecase: llmUsecase, llmUsecase: llmUsecase,
modelUsecase: modelUsecase, modelRepo: modelRepo,
} }
if err := consumer.RegisterHandler(domain.VectorTaskTopic, h.HandleNodeContentVectorRequest); err != nil { if err := consumer.RegisterHandler(domain.VectorTaskTopic, h.HandleNodeContentVectorRequest); err != nil {
return nil, err return nil, err
@ -134,13 +134,11 @@ func (h *RAGMQHandler) HandleNodeContentVectorRequest(ctx context.Context, msg t
h.logger.Info("node is folder, skip summary", log.Any("node_id", request.NodeID)) h.logger.Info("node is folder, skip summary", log.Any("node_id", request.NodeID))
return nil return nil
} }
model, err := h.modelRepo.GetChatModel(ctx)
model, err := h.modelUsecase.GetChatModel(ctx)
if err != nil { if err != nil {
h.logger.Error("get chat model failed", log.Error(err)) h.logger.Error("get chat model failed", log.Error(err))
return nil return nil
} }
summary, err := h.llmUsecase.SummaryNode(ctx, model, node.Name, node.Content) summary, err := h.llmUsecase.SummaryNode(ctx, model, node.Name, node.Content)
if err != nil { if err != nil {
h.logger.Error("summary node content failed", log.Error(err)) h.logger.Error("summary node content failed", log.Error(err))

View File

@ -40,8 +40,6 @@ func NewModelHandler(echo *echo.Echo, baseHandler *handler.BaseHandler, logger *
group.POST("/check", handler.CheckModel) group.POST("/check", handler.CheckModel)
group.POST("/provider/supported", handler.GetProviderSupportedModelList) group.POST("/provider/supported", handler.GetProviderSupportedModelList)
group.PUT("", handler.UpdateModel) group.PUT("", handler.UpdateModel)
group.POST("/switch-mode", handler.SwitchMode)
group.GET("/mode-setting", handler.GetModelModeSetting)
return handler return handler
} }
@ -213,58 +211,3 @@ func (h *ModelHandler) GetProviderSupportedModelList(c echo.Context) error {
} }
return h.NewResponseWithData(c, models) return h.NewResponseWithData(c, models)
} }
// SwitchMode
//
// @Summary switch mode
// @Description switch model mode between manual and auto
// @Tags model
// @Accept json
// @Produce json
// @Param request body domain.SwitchModeReq true "switch mode request"
// @Success 200 {object} domain.Response{data=domain.SwitchModeResp}
// @Router /api/v1/model/switch-mode [post]
func (h *ModelHandler) SwitchMode(c echo.Context) error {
var req domain.SwitchModeReq
if err := c.Bind(&req); err != nil {
return h.NewResponseWithError(c, "bind request failed", err)
}
if err := c.Validate(&req); err != nil {
return h.NewResponseWithError(c, "validate request failed", err)
}
ctx := c.Request().Context()
if err := h.usecase.SwitchMode(ctx, &req); err != nil {
return h.NewResponseWithError(c, err.Error(), err)
}
resp := &domain.SwitchModeResp{
Message: "模式切换成功",
}
return h.NewResponseWithData(c, resp)
}
// GetModelModeSetting
//
// @Summary get model mode setting
// @Description get current model mode setting including mode, API key and chat model
// @Tags model
// @Accept json
// @Produce json
// @Success 200 {object} domain.Response{data=domain.ModelModeSetting}
// @Router /api/v1/model/mode-setting [get]
func (h *ModelHandler) GetModelModeSetting(c echo.Context) error {
ctx := c.Request().Context()
setting, err := h.usecase.GetModelModeSetting(ctx)
if err != nil {
// 如果获取失败,返回默认值(手动模式)
h.logger.Warn("failed to get model mode setting, return default", log.Error(err))
defaultSetting := domain.ModelModeSetting{
Mode: consts.ModelSettingModeManual,
AutoModeAPIKey: "",
ChatModel: "",
}
return h.NewResponseWithData(c, defaultSetting)
}
return h.NewResponseWithData(c, setting)
}

@ -1 +1 @@
Subproject commit c4dc498df094cb617d31c95580db8239a445d652 Subproject commit 3f6d9b2aca901c53abfcad18f244496843eda3eb

View File

@ -23,5 +23,4 @@ var ProviderSet = wire.NewSet(
NewAuthRepo, NewAuthRepo,
NewWechatRepository, NewWechatRepository,
NewAPITokenRepo, NewAPITokenRepo,
NewSystemSettingRepo,
) )

View File

@ -1,35 +0,0 @@
package pg
import (
"context"
"github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/log"
"github.com/chaitin/panda-wiki/store/pg"
)
type SystemSettingRepo struct {
db *pg.DB
logger *log.Logger
}
func NewSystemSettingRepo(db *pg.DB, logger *log.Logger) *SystemSettingRepo {
return &SystemSettingRepo{
db: db,
logger: logger.WithModule("repo.pg.system_setting"),
}
}
func (r *SystemSettingRepo) GetSystemSetting(ctx context.Context, key string) (*domain.SystemSetting, error) {
var setting domain.SystemSetting
result := r.db.WithContext(ctx).Where("key = ?", key).First(&setting)
if result.Error != nil {
return nil, result.Error
}
return &setting, nil
}
func (r *SystemSettingRepo) UpdateSystemSetting(ctx context.Context, key, value string) error {
return r.db.WithContext(ctx).Model(&domain.SystemSetting{}).Where("key = ?", key).Update("value", value).Error
}

View File

@ -1,4 +0,0 @@
-- Drop settings table
DROP TABLE IF EXISTS system_settings;
-- drop index
DROP INDEX IF EXISTS idx_system_settings_key;

View File

@ -1,30 +0,0 @@
-- Create settings table
CREATE TABLE IF NOT EXISTS system_settings (
id SERIAL PRIMARY KEY,
key TEXT NOT NULL,
value JSONB NOT NULL,
description TEXT,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_uniq_system_settings_key ON system_settings(key);
-- Insert model_setting_mode setting
-- If there are existing knowledge bases, set mode to 'manual', otherwise set to 'auto'
INSERT INTO system_settings (key, value, description)
SELECT
'model_setting_mode',
jsonb_build_object(
'mode', CASE
WHEN EXISTS (SELECT 1 FROM knowledge_bases LIMIT 1) THEN 'manual'
ELSE 'auto'
END,
'auto_mode_api_key', '',
'chat_model', '',
'is_manual_embedding_updated', false
),
'Model setting mode configuration'
WHERE NOT EXISTS (
SELECT 1 FROM system_settings WHERE key = 'model_setting_mode'
);

View File

@ -84,7 +84,7 @@ func (u *CreationUsecase) TextCreation(ctx context.Context, req *domain.TextReq,
func (u *CreationUsecase) TabComplete(ctx context.Context, req *domain.CompleteReq) (string, error) { func (u *CreationUsecase) TabComplete(ctx context.Context, req *domain.CompleteReq) (string, error) {
// For FIM (Fill in Middle) style completion, we need to handle prefix and suffix // For FIM (Fill in Middle) style completion, we need to handle prefix and suffix
if req.Prefix != "" || req.Suffix != "" { if req.Prefix != "" || req.Suffix != "" {
model, err := u.model.GetChatModel(ctx) model, err := u.model.GetModelByType(ctx, domain.ModelTypeChat)
if err != nil { if err != nil {
u.logger.Error("get chat model failed", log.Error(err)) u.logger.Error("get chat model failed", log.Error(err))
return "", domain.ErrModelNotConfigured return "", domain.ErrModelNotConfigured

View File

@ -2,15 +2,14 @@ package usecase
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"time"
"github.com/cloudwego/eino/schema" "github.com/cloudwego/eino/schema"
"github.com/google/uuid"
"github.com/samber/lo"
modelkitDomain "github.com/chaitin/ModelKit/v2/domain"
modelkit "github.com/chaitin/ModelKit/v2/usecase"
"github.com/chaitin/panda-wiki/config" "github.com/chaitin/panda-wiki/config"
"github.com/chaitin/panda-wiki/consts"
"github.com/chaitin/panda-wiki/domain" "github.com/chaitin/panda-wiki/domain"
"github.com/chaitin/panda-wiki/log" "github.com/chaitin/panda-wiki/log"
"github.com/chaitin/panda-wiki/repo/mq" "github.com/chaitin/panda-wiki/repo/mq"
@ -19,47 +18,118 @@ import (
) )
type ModelUsecase struct { type ModelUsecase struct {
modelRepo *pg.ModelRepository modelRepo *pg.ModelRepository
logger *log.Logger logger *log.Logger
config *config.Config config *config.Config
nodeRepo *pg.NodeRepository nodeRepo *pg.NodeRepository
ragRepo *mq.RAGRepository ragRepo *mq.RAGRepository
ragStore rag.RAGService ragStore rag.RAGService
kbRepo *pg.KnowledgeBaseRepository kbRepo *pg.KnowledgeBaseRepository
systemSettingRepo *pg.SystemSettingRepo
modelkit *modelkit.ModelKit
} }
func NewModelUsecase(modelRepo *pg.ModelRepository, nodeRepo *pg.NodeRepository, ragRepo *mq.RAGRepository, ragStore rag.RAGService, logger *log.Logger, config *config.Config, kbRepo *pg.KnowledgeBaseRepository, settingRepo *pg.SystemSettingRepo) *ModelUsecase { func NewModelUsecase(modelRepo *pg.ModelRepository, nodeRepo *pg.NodeRepository, ragRepo *mq.RAGRepository, ragStore rag.RAGService, logger *log.Logger, config *config.Config, kbRepo *pg.KnowledgeBaseRepository) *ModelUsecase {
modelkit := modelkit.NewModelKit(logger.Logger)
u := &ModelUsecase{ u := &ModelUsecase{
modelRepo: modelRepo, modelRepo: modelRepo,
logger: logger.WithModule("usecase.model"), logger: logger.WithModule("usecase.model"),
config: config, config: config,
nodeRepo: nodeRepo, nodeRepo: nodeRepo,
ragRepo: ragRepo, ragRepo: ragRepo,
ragStore: ragStore, ragStore: ragStore,
kbRepo: kbRepo, kbRepo: kbRepo,
systemSettingRepo: settingRepo, }
modelkit: modelkit, if err := u.initEmbeddingAndRerankModel(context.Background()); err != nil {
logger.Error("init embedding & rerank & analysis model failed", log.Any("error", err))
} }
return u return u
} }
func (u *ModelUsecase) Create(ctx context.Context, model *domain.Model) error { func (u *ModelUsecase) initEmbeddingAndRerankModel(ctx context.Context) error {
var updatedEmbeddingModel bool isReady := false
if model.Type == domain.ModelTypeEmbedding { // wait for raglite to be ready
updatedEmbeddingModel = true for range 60 {
models, err := u.ragStore.GetModelList(ctx)
if err != nil {
u.logger.Error("wait for raglite to be ready", log.Any("error", err))
time.Sleep(1 * time.Second)
continue
}
isReady = true
if len(models) > 0 {
// init analysis model for old user
hasAnalysis := false
for _, m := range models {
if m.Type == domain.ModelTypeAnalysis {
hasAnalysis = true
break
}
}
if !hasAnalysis {
if err := u.createAndSyncModelToRAGLite(ctx, "qwen2.5-3b-instruct", domain.ModelTypeAnalysis); err != nil {
return fmt.Errorf("add analysis model err: %v", err)
}
}
return nil
} else {
break
}
} }
if !isReady {
return fmt.Errorf("raglite is not ready")
}
if err := u.createAndSyncModelToRAGLite(ctx, "bge-m3", domain.ModelTypeEmbedding); err != nil {
return fmt.Errorf("create and sync model err: %v", err)
}
if err := u.createAndSyncModelToRAGLite(ctx, "bge-reranker-v2-m3", domain.ModelTypeRerank); err != nil {
return fmt.Errorf("create and sync model err: %v", err)
}
if err := u.createAndSyncModelToRAGLite(ctx, "qwen2.5-3b-instruct", domain.ModelTypeAnalysis); err != nil {
return fmt.Errorf("create and sync model err: %v", err)
}
return nil
}
func (u *ModelUsecase) createAndSyncModelToRAGLite(ctx context.Context, modelName string, modelType domain.ModelType) error {
// FIXME: just for test, remove it later
// shared_key by BaiZhiCloud
sharedKey := "sk-r8tmBtcU1JotPDPnlgZLOY4Z6Dbb7FufcSeTkFpRWA5v4Llr"
baseURL := "https://model-square.app.baizhi.cloud/v1"
model := &domain.Model{
ID: uuid.New().String(),
Provider: domain.ModelProviderBrandBaiZhiCloud,
Model: modelName,
APIKey: sharedKey,
APIHeader: "",
BaseURL: baseURL,
IsActive: true,
APIVersion: "",
Type: modelType,
}
id, err := u.ragStore.AddModel(ctx, model)
if err != nil {
return fmt.Errorf("init %s model failed: %w", modelName, err)
}
model.ID = id
if err := u.modelRepo.Create(ctx, model); err != nil {
return fmt.Errorf("create %s model failed: %w", modelName, err)
}
return nil
}
func (u *ModelUsecase) Create(ctx context.Context, model *domain.Model) error {
if err := u.modelRepo.Create(ctx, model); err != nil { if err := u.modelRepo.Create(ctx, model); err != nil {
return err return err
} }
// 模型更新成功后,如果更新嵌入模型,则触发记录更新 if model.Type == domain.ModelTypeEmbedding || model.Type == domain.ModelTypeRerank || model.Type == domain.ModelTypeAnalysis || model.Type == domain.ModelTypeAnalysisVL {
if updatedEmbeddingModel { if id, err := u.ragStore.AddModel(ctx, model); err != nil {
if _, err := u.updateModeSettingConfig(ctx, "", "", "", true); err != nil {
return err return err
} else {
model.ID = id
} }
} }
if model.Type == domain.ModelTypeEmbedding {
return u.TriggerUpsertRecords(ctx)
}
return nil return nil
} }
@ -108,50 +178,44 @@ func (u *ModelUsecase) TriggerUpsertRecords(ctx context.Context) error {
} }
func (u *ModelUsecase) Update(ctx context.Context, req *domain.UpdateModelReq) error { func (u *ModelUsecase) Update(ctx context.Context, req *domain.UpdateModelReq) error {
var updatedEmbeddingModel bool
if req.Type == domain.ModelTypeEmbedding {
updatedEmbeddingModel = true
}
if err := u.modelRepo.Update(ctx, req); err != nil { if err := u.modelRepo.Update(ctx, req); err != nil {
return err return err
} }
// 模型更新成功后,如果更新嵌入模型,则触发记录更新 ragModelTypes := []domain.ModelType{
if updatedEmbeddingModel { domain.ModelTypeEmbedding,
if _, err := u.updateModeSettingConfig(ctx, "", "", "", true); err != nil { domain.ModelTypeRerank,
domain.ModelTypeAnalysis,
domain.ModelTypeAnalysisVL,
}
if lo.Contains(ragModelTypes, req.Type) {
updateModel := &domain.Model{
ID: req.ID,
Model: req.Model,
Type: req.Type,
BaseURL: req.BaseURL,
APIKey: req.APIKey,
IsActive: true,
}
if req.Parameters != nil {
updateModel.Parameters = *req.Parameters
}
// update is active flag for analysis models
if (req.Type == domain.ModelTypeAnalysis || req.Type == domain.ModelTypeAnalysisVL) && req.IsActive != nil {
updateModel.IsActive = *req.IsActive
}
if err := u.ragStore.UpdateModel(ctx, updateModel); err != nil {
return err return err
} }
} }
// update all records when embedding model is updated
if req.Type == domain.ModelTypeEmbedding {
return u.TriggerUpsertRecords(ctx)
}
return nil return nil
} }
func (u *ModelUsecase) GetChatModel(ctx context.Context) (*domain.Model, error) { func (u *ModelUsecase) GetChatModel(ctx context.Context) (*domain.Model, error) {
var model *domain.Model return u.modelRepo.GetChatModel(ctx)
modelModeSetting, err := u.GetModelModeSetting(ctx)
// 获取不到模型模式时,使用手动模式, 不返回错误
if err != nil {
u.logger.Error("get model mode setting failed, use manual mode", log.Error(err))
}
if err == nil && modelModeSetting.Mode == consts.ModelSettingModeAuto && modelModeSetting.AutoModeAPIKey != "" {
modelName := modelModeSetting.ChatModel
if modelName == "" {
modelName = string(consts.AutoModeDefaultChatModel)
}
model = &domain.Model{
Model: modelName,
Type: domain.ModelTypeChat,
IsActive: true,
BaseURL: consts.AutoModeBaseURL,
APIKey: modelModeSetting.AutoModeAPIKey,
Provider: domain.ModelProviderBrandBaiZhiCloud,
}
return model, nil
}
model, err = u.modelRepo.GetChatModel(ctx)
if err != nil {
return nil, err
}
return model, nil
} }
func (u *ModelUsecase) GetModelByType(ctx context.Context, modelType domain.ModelType) (*domain.Model, error) { func (u *ModelUsecase) GetModelByType(ctx context.Context, modelType domain.ModelType) (*domain.Model, error) {
@ -161,175 +225,3 @@ func (u *ModelUsecase) GetModelByType(ctx context.Context, modelType domain.Mode
func (u *ModelUsecase) UpdateUsage(ctx context.Context, modelID string, usage *schema.TokenUsage) error { func (u *ModelUsecase) UpdateUsage(ctx context.Context, modelID string, usage *schema.TokenUsage) error {
return u.modelRepo.UpdateUsage(ctx, modelID, usage) return u.modelRepo.UpdateUsage(ctx, modelID, usage)
} }
func (u *ModelUsecase) SwitchMode(ctx context.Context, req *domain.SwitchModeReq) error {
// 只有配置正确才能切换模式
if req.Mode == string(consts.ModelSettingModeAuto) {
if req.AutoModeAPIKey == "" {
return fmt.Errorf("auto mode api key is required")
}
modelName := req.ChatModel
if modelName == "" {
modelName = consts.GetAutoModeDefaultModel(string(domain.ModelTypeChat))
}
// 检查 API Key 是否有效
check, err := u.modelkit.CheckModel(ctx, &modelkitDomain.CheckModelReq{
Provider: string(domain.ModelProviderBrandBaiZhiCloud),
Model: modelName,
BaseURL: consts.AutoModeBaseURL,
APIKey: req.AutoModeAPIKey,
Type: string(domain.ModelTypeChat),
})
if err != nil {
return fmt.Errorf("百智云模型 API Key 检查失败: %w", err)
}
if check.Error != "" {
return fmt.Errorf("百智云模型 API Key 检查失败: %s", check.Error)
}
} else {
needModelTypes := []domain.ModelType{
domain.ModelTypeChat,
domain.ModelTypeEmbedding,
domain.ModelTypeRerank,
domain.ModelTypeAnalysis,
}
for _, modelType := range needModelTypes {
if _, err := u.modelRepo.GetModelByType(ctx, modelType); err != nil {
return fmt.Errorf("需要配置 %s 模型", modelType)
}
}
}
oldModelModeSetting, err := u.GetModelModeSetting(ctx)
if err != nil {
return err
}
var isResetEmbeddingUpdateFlag = true
// 只有切换手动模式时重置isManualEmbeddingUpdated为false
if req.Mode == string(consts.ModelSettingModeManual) {
isResetEmbeddingUpdateFlag = false
}
modelModeSetting, err := u.updateModeSettingConfig(ctx, req.Mode, req.AutoModeAPIKey, req.ChatModel, isResetEmbeddingUpdateFlag)
if err != nil {
return err
}
return u.updateRAGModelsByMode(ctx, req.Mode, modelModeSetting.AutoModeAPIKey, oldModelModeSetting)
}
// updateModeSettingConfig 读取当前设置并更新,然后持久化
func (u *ModelUsecase) updateModeSettingConfig(ctx context.Context, mode, apiKey, chatModel string, isManualEmbeddingUpdated bool) (*domain.ModelModeSetting, error) {
// 读取当前设置
setting, err := u.systemSettingRepo.GetSystemSetting(ctx, string(consts.SystemSettingModelMode))
if err != nil {
return nil, fmt.Errorf("failed to get current model setting: %w", err)
}
var config domain.ModelModeSetting
if err := json.Unmarshal(setting.Value, &config); err != nil {
return nil, fmt.Errorf("failed to parse current model setting: %w", err)
}
// 更新设置
if apiKey != "" {
config.AutoModeAPIKey = apiKey
}
if chatModel != "" {
config.ChatModel = chatModel
}
if mode != "" {
config.Mode = consts.ModelSettingMode(mode)
}
config.IsManualEmbeddingUpdated = isManualEmbeddingUpdated
// 持久化设置
updatedValue, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("failed to marshal updated model setting: %w", err)
}
if err := u.systemSettingRepo.UpdateSystemSetting(ctx, string(consts.SystemSettingModelMode), string(updatedValue)); err != nil {
return nil, fmt.Errorf("failed to update model setting: %w", err)
}
return &config, nil
}
func (u *ModelUsecase) GetModelModeSetting(ctx context.Context) (domain.ModelModeSetting, error) {
setting, err := u.systemSettingRepo.GetSystemSetting(ctx, string(consts.SystemSettingModelMode))
if err != nil {
return domain.ModelModeSetting{}, fmt.Errorf("failed to get model mode setting: %w", err)
}
var config domain.ModelModeSetting
if err := json.Unmarshal(setting.Value, &config); err != nil {
return domain.ModelModeSetting{}, fmt.Errorf("failed to parse model mode setting: %w", err)
}
// 无效设置检查
if config == (domain.ModelModeSetting{}) || config.Mode == "" {
return domain.ModelModeSetting{}, fmt.Errorf("model mode setting is invalid")
}
return config, nil
}
// updateRAGModelsByMode 根据模式更新 RAG 模型embedding、rerank、analysis、analysisVL
func (u *ModelUsecase) updateRAGModelsByMode(ctx context.Context, mode, autoModeAPIKey string, oldModelModeSetting domain.ModelModeSetting) error {
var isTriggerUpsertRecords = true
// 手动切换到手动模式, 根据IsManualEmbeddingUpdated字段决定
if oldModelModeSetting.Mode == consts.ModelSettingModeManual && mode == string(consts.ModelSettingModeManual) {
isTriggerUpsertRecords = oldModelModeSetting.IsManualEmbeddingUpdated
}
ragModelTypes := []domain.ModelType{
domain.ModelTypeEmbedding,
domain.ModelTypeRerank,
domain.ModelTypeAnalysis,
domain.ModelTypeAnalysisVL,
}
for _, modelType := range ragModelTypes {
var model *domain.Model
if mode == string(consts.ModelSettingModeManual) {
// 获取该类型的活跃模型
m, err := u.modelRepo.GetModelByType(ctx, modelType)
if err != nil {
u.logger.Warn("failed to get model by type", log.String("type", string(modelType)), log.Any("error", err))
continue
}
if m == nil || !m.IsActive {
u.logger.Warn("no active model found for type", log.String("type", string(modelType)))
continue
}
model = m
} else {
modelName := consts.GetAutoModeDefaultModel(string(modelType))
model = &domain.Model{
Model: modelName,
Type: modelType,
IsActive: true,
BaseURL: consts.AutoModeBaseURL,
APIKey: autoModeAPIKey,
Provider: domain.ModelProviderBrandBaiZhiCloud,
}
}
// 更新RAG存储中的模型
if model != nil {
// rag store中更新失败不影响其他模型更新
if err := u.ragStore.UpdateModel(ctx, model); err != nil {
u.logger.Error("failed to update model in RAG store", log.String("model_id", model.ID), log.String("type", string(modelType)), log.Any("error", err))
continue
}
u.logger.Info("successfully updated RAG model", log.String("model name: ", string(model.Model)))
}
}
// 触发记录更新
if isTriggerUpsertRecords {
u.logger.Info("embedding model updated, triggering upsert records")
return u.TriggerUpsertRecords(ctx)
}
return nil
}

View File

@ -27,18 +27,17 @@ import (
) )
type NodeUsecase struct { type NodeUsecase struct {
nodeRepo *pg.NodeRepository nodeRepo *pg.NodeRepository
appRepo *pg.AppRepository appRepo *pg.AppRepository
ragRepo *mq.RAGRepository ragRepo *mq.RAGRepository
kbRepo *pg.KnowledgeBaseRepository kbRepo *pg.KnowledgeBaseRepository
modelRepo *pg.ModelRepository modelRepo *pg.ModelRepository
userRepo *pg.UserRepository userRepo *pg.UserRepository
authRepo *pg.AuthRepo authRepo *pg.AuthRepo
llmUsecase *LLMUsecase llmUsecase *LLMUsecase
logger *log.Logger logger *log.Logger
s3Client *s3.MinioClient s3Client *s3.MinioClient
rAGService rag.RAGService rAGService rag.RAGService
modelUsecase *ModelUsecase
} }
func NewNodeUsecase( func NewNodeUsecase(
@ -53,21 +52,19 @@ func NewNodeUsecase(
s3Client *s3.MinioClient, s3Client *s3.MinioClient,
modelRepo *pg.ModelRepository, modelRepo *pg.ModelRepository,
authRepo *pg.AuthRepo, authRepo *pg.AuthRepo,
modelUsecase *ModelUsecase,
) *NodeUsecase { ) *NodeUsecase {
return &NodeUsecase{ return &NodeUsecase{
nodeRepo: nodeRepo, nodeRepo: nodeRepo,
rAGService: ragService, rAGService: ragService,
appRepo: appRepo, appRepo: appRepo,
ragRepo: ragRepo, ragRepo: ragRepo,
kbRepo: kbRepo, kbRepo: kbRepo,
authRepo: authRepo, authRepo: authRepo,
userRepo: userRepo, userRepo: userRepo,
llmUsecase: llmUsecase, llmUsecase: llmUsecase,
modelRepo: modelRepo, modelRepo: modelRepo,
logger: logger.WithModule("usecase.node"), logger: logger.WithModule("usecase.node"),
s3Client: s3Client, s3Client: s3Client,
modelUsecase: modelUsecase,
} }
} }
@ -223,7 +220,7 @@ func (u *NodeUsecase) MoveNode(ctx context.Context, req *domain.MoveNodeReq) err
} }
func (u *NodeUsecase) SummaryNode(ctx context.Context, req *domain.NodeSummaryReq) (string, error) { func (u *NodeUsecase) SummaryNode(ctx context.Context, req *domain.NodeSummaryReq) (string, error) {
model, err := u.modelUsecase.GetChatModel(ctx) model, err := u.modelRepo.GetChatModel(ctx)
if err != nil { if err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
return "", domain.ErrModelNotConfigured return "", domain.ErrModelNotConfigured

View File

@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Box, Stepper, Step, StepLabel } from '@mui/material'; import { Box, Stepper, Step, StepLabel } from '@mui/material';
import { Modal, message } from '@ctzhian/ui'; import { Modal } from '@ctzhian/ui';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { import {
setKbC, setKbC,
@ -10,20 +10,18 @@ import {
import { useAppSelector, useAppDispatch } from '@/store'; import { useAppSelector, useAppDispatch } from '@/store';
import { postApiV1KnowledgeBaseRelease } from '@/request/KnowledgeBase'; import { postApiV1KnowledgeBaseRelease } from '@/request/KnowledgeBase';
import { import {
Step1Model, Step1Config,
Step2Config, Step2Import,
Step3Import, Step3Publish,
Step4Publish, Step4Test,
Step5Test, Step5Decorate,
Step6Decorate, Step6Complete,
Step7Complete,
} from './steps'; } from './steps';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
// Remove interface as we're using Redux state // Remove interface as we're using Redux state
const steps = [ const steps = [
'模型配置',
'配置监听', '配置监听',
'录入文档', '录入文档',
'发布内容', '发布内容',
@ -33,19 +31,20 @@ const steps = [
]; ];
const CreateWikiModal = () => { const CreateWikiModal = () => {
const { kb_c, kb_id, kbList } = useAppSelector(state => state.config); const { kb_c, kb_id, kbList, modelStatus } = useAppSelector(
state => state.config,
);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const location = useLocation(); const location = useLocation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [activeStep, setActiveStep] = useState(0); const [activeStep, setActiveStep] = useState(0);
const [nodeIds, setNodeIds] = useState<string[]>([]); const [nodeIds, setNodeIds] = useState<string[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const Step1ModelRef = useRef<{ onSubmit: () => Promise<void> }>(null); const step1ConfigRef = useRef<{ onSubmit: () => Promise<void> }>(null);
const step2ConfigRef = useRef<{ onSubmit: () => Promise<void> }>(null); const step2ImportRef = useRef<{
const step3ImportRef = useRef<{
onSubmit: () => Promise<Record<'id', string>[]>; onSubmit: () => Promise<Record<'id', string>[]>;
}>(null); }>(null);
const step6DecorateRef = useRef<{ onSubmit: () => Promise<void> }>(null); const step5DecorateRef = useRef<{ onSubmit: () => Promise<void> }>(null);
const onCancel = () => { const onCancel = () => {
dispatch(setKbC(false)); dispatch(setKbC(false));
@ -67,30 +66,17 @@ const CreateWikiModal = () => {
const handleNext = () => { const handleNext = () => {
if (activeStep === 0) { if (activeStep === 0) {
setLoading(true); setLoading(true);
Step1ModelRef.current step1ConfigRef.current
?.onSubmit?.() ?.onSubmit?.()
.then(() => { .then(() => {
setActiveStep(prev => prev + 1); setActiveStep(prev => prev + 1);
}) })
.catch(error => {
message.error(error.message || '模型配置验证失败');
})
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
}); });
} else if (activeStep === 1) { } else if (activeStep === 1) {
setLoading(true); setLoading(true);
step2ConfigRef.current step2ImportRef.current
?.onSubmit?.()
.then(() => {
setActiveStep(prev => prev + 1);
})
.finally(() => {
setLoading(false);
});
} else if (activeStep === 2) {
setLoading(true);
step3ImportRef.current
?.onSubmit?.() ?.onSubmit?.()
.then(res => { .then(res => {
setNodeIds(res.map(item => item.id)); setNodeIds(res.map(item => item.id));
@ -99,17 +85,17 @@ const CreateWikiModal = () => {
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
}); });
} else if (activeStep === 3) { } else if (activeStep === 2) {
setLoading(true); setLoading(true);
onPublish().finally(() => { onPublish().finally(() => {
setActiveStep(prev => prev + 1); setActiveStep(prev => prev + 1);
setLoading(false); setLoading(false);
}); });
} else if (activeStep === 4) { } else if (activeStep === 3) {
setActiveStep(prev => prev + 1); setActiveStep(prev => prev + 1);
} else if (activeStep === 5) { } else if (activeStep === 4) {
setLoading(true); setLoading(true);
step6DecorateRef.current step5DecorateRef.current
?.onSubmit?.() ?.onSubmit?.()
.then(() => { .then(() => {
setActiveStep(prev => prev + 1); setActiveStep(prev => prev + 1);
@ -117,7 +103,7 @@ const CreateWikiModal = () => {
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
}); });
} else if (activeStep === 6) { } else if (activeStep === 5) {
onCancel(); onCancel();
} }
}; };
@ -131,19 +117,17 @@ const CreateWikiModal = () => {
const renderStepContent = () => { const renderStepContent = () => {
switch (activeStep) { switch (activeStep) {
case 0: case 0:
return <Step1Model ref={Step1ModelRef} />; return <Step1Config ref={step1ConfigRef} />;
case 1: case 1:
return <Step2Config ref={step2ConfigRef} />; return <Step2Import ref={step2ImportRef} />;
case 2: case 2:
return <Step3Import ref={step3ImportRef} />; return <Step3Publish />;
case 3: case 3:
return <Step4Publish />; return <Step4Test />;
case 4: case 4:
return <Step5Test />; return <Step5Decorate ref={step5DecorateRef} nodeIds={nodeIds} />;
case 5: case 5:
return <Step6Decorate ref={step6DecorateRef} nodeIds={nodeIds} />; return <Step6Complete />;
case 6:
return <Step7Complete />;
default: default:
return null; return null;
} }
@ -164,8 +148,8 @@ const CreateWikiModal = () => {
}, [kb_c]); }, [kb_c]);
useEffect(() => { useEffect(() => {
if (kbList?.length === 0) setOpen(true); if (kbList?.length === 0 && modelStatus) setOpen(true);
}, [kbList]); }, [kbList, modelStatus]);
return ( return (
<Modal <Modal

View File

@ -73,11 +73,11 @@ const VALIDATION_RULES = {
}, },
}; };
interface Step2ConfigProps { interface Step1ConfigProps {
ref: Ref<{ onSubmit: () => Promise<unknown> }>; ref: Ref<{ onSubmit: () => Promise<unknown> }>;
} }
const Step2Config: React.FC<Step2ConfigProps> = ({ ref }) => { const Step1Config: React.FC<Step1ConfigProps> = ({ ref }) => {
const { const {
control, control,
formState: { errors }, formState: { errors },
@ -358,4 +358,4 @@ const Step2Config: React.FC<Step2ConfigProps> = ({ ref }) => {
); );
}; };
export default Step2Config; export default Step1Config;

View File

@ -1,120 +0,0 @@
import React, {
useState,
useImperativeHandle,
Ref,
useEffect,
useRef,
} from 'react';
import { Box } from '@mui/material';
import { useAppSelector, useAppDispatch } from '@/store';
import { setModelList } from '@/store/slices/config';
import { getApiV1ModelList, getApiV1ModelModeSetting } from '@/request/Model';
import { GithubComChaitinPandaWikiDomainModelListItem } from '@/request/types';
import ModelConfig, {
ModelConfigRef,
} from '@/components/System/component/ModelConfig';
interface Step1ModelProps {
ref: Ref<{ onSubmit: () => Promise<void> }>;
}
const Step1Model: React.FC<Step1ModelProps> = ({ ref }) => {
const { modelList } = useAppSelector(state => state.config);
const dispatch = useAppDispatch();
const modelConfigRef = useRef<ModelConfigRef>(null);
const [chatModelData, setChatModelData] =
useState<GithubComChaitinPandaWikiDomainModelListItem | null>(null);
const [embeddingModelData, setEmbeddingModelData] =
useState<GithubComChaitinPandaWikiDomainModelListItem | null>(null);
const [rerankModelData, setRerankModelData] =
useState<GithubComChaitinPandaWikiDomainModelListItem | null>(null);
const [analysisModelData, setAnalysisModelData] =
useState<GithubComChaitinPandaWikiDomainModelListItem | null>(null);
const [analysisVLModelData, setAnalysisVLModelData] =
useState<GithubComChaitinPandaWikiDomainModelListItem | null>(null);
const getModelList = () => {
getApiV1ModelList().then(res => {
dispatch(
setModelList(res as GithubComChaitinPandaWikiDomainModelListItem[]),
);
});
};
const handleModelList = (
list: GithubComChaitinPandaWikiDomainModelListItem[],
) => {
const chat = list.find(it => it.type === 'chat') || null;
const embedding = list.find(it => it.type === 'embedding') || null;
const rerank = list.find(it => it.type === 'rerank') || null;
const analysis = list.find(it => it.type === 'analysis') || null;
const analysisVL = list.find(it => it.type === 'analysis-vl') || null;
setChatModelData(chat);
setEmbeddingModelData(embedding);
setRerankModelData(rerank);
setAnalysisModelData(analysis);
setAnalysisVLModelData(analysisVL);
};
useEffect(() => {
if (modelList) {
handleModelList(modelList);
}
}, [modelList]);
const onSubmit = async () => {
// 检查模型模式设置
try {
const modeSetting = await getApiV1ModelModeSetting();
// 如果是 auto 模式,检查是否配置了 API key
if (modeSetting?.mode === 'auto') {
if (!modeSetting.auto_mode_api_key) {
return Promise.reject(new Error('请点击应用完成模型配置'));
}
} else {
// 手动模式检查
if (
!chatModelData ||
!embeddingModelData ||
!rerankModelData ||
!analysisModelData
) {
return Promise.reject(new Error('请配置必要的模型后点击应用'));
}
}
} catch (error) {
if (error instanceof Error) {
return Promise.reject(error);
}
return Promise.reject(new Error('配置模型失败'));
}
return Promise.resolve();
};
useImperativeHandle(ref, () => ({
onSubmit,
}));
return (
<Box>
<ModelConfig
ref={modelConfigRef}
onCloseModal={() => {}}
chatModelData={chatModelData}
embeddingModelData={embeddingModelData}
rerankModelData={rerankModelData}
analysisModelData={analysisModelData}
analysisVLModelData={analysisVLModelData}
getModelList={getModelList}
hideDocumentationHint={true}
showTip={true}
/>
</Box>
);
};
export default Step1Model;

View File

@ -5,11 +5,11 @@ import { postApiV1Node } from '@/request/Node';
import { INIT_DOC_DATA } from './initData'; import { INIT_DOC_DATA } from './initData';
import { useAppSelector } from '@/store'; import { useAppSelector } from '@/store';
interface Step3ImportProps { interface Step2ImportProps {
ref: Ref<{ onSubmit: () => Promise<Record<'id', string>[]> }>; ref: Ref<{ onSubmit: () => Promise<Record<'id', string>[]> }>;
} }
const Step3Import: React.FC<Step3ImportProps> = ({ ref }) => { const Step2Import: React.FC<Step2ImportProps> = ({ ref }) => {
const { kb_id } = useAppSelector(state => state.config); const { kb_id } = useAppSelector(state => state.config);
const onSubmit = () => { const onSubmit = () => {
return Promise.all( return Promise.all(
@ -42,4 +42,4 @@ const Step3Import: React.FC<Step3ImportProps> = ({ ref }) => {
); );
}; };
export default Step3Import; export default Step2Import;

View File

@ -1,7 +1,7 @@
import { Box, Stack, FormControlLabel, Checkbox } from '@mui/material'; import { Box, Stack, FormControlLabel, Checkbox } from '@mui/material';
import publish from '@/assets/images/init/publish.png'; import publish from '@/assets/images/init/publish.png';
const Step4Publish = () => { const Step3Publish = () => {
return ( return (
<Stack gap={2} sx={{ textAlign: 'center', py: 4 }}> <Stack gap={2} sx={{ textAlign: 'center', py: 4 }}>
<Box component='img' src={publish} sx={{ width: '100%' }}></Box> <Box component='img' src={publish} sx={{ width: '100%' }}></Box>
@ -18,4 +18,4 @@ const Step4Publish = () => {
); );
}; };
export default Step4Publish; export default Step3Publish;

View File

@ -1,7 +1,7 @@
import { Box, Stack } from '@mui/material'; import { Box, Stack } from '@mui/material';
import test from '@/assets/images/init/test.png'; import test from '@/assets/images/init/test.png';
const Step5Test = () => { const Step4Test = () => {
return ( return (
<Stack gap={2} sx={{ textAlign: 'center', py: 4 }}> <Stack gap={2} sx={{ textAlign: 'center', py: 4 }}>
<Box component='img' src={test} sx={{ width: '100%' }}></Box> <Box component='img' src={test} sx={{ width: '100%' }}></Box>
@ -9,4 +9,4 @@ const Step5Test = () => {
); );
}; };
export default Step5Test; export default Step4Test;

View File

@ -5,12 +5,12 @@ import { INIT_LADING_DATA } from './initData';
import { getApiV1AppDetail, putApiV1App } from '@/request/App'; import { getApiV1AppDetail, putApiV1App } from '@/request/App';
import { useAppSelector } from '@/store'; import { useAppSelector } from '@/store';
interface Step6DecorateProps { interface Step5DecorateProps {
ref: Ref<{ onSubmit: () => void }>; ref: Ref<{ onSubmit: () => void }>;
nodeIds: string[]; nodeIds: string[];
} }
const Step6Decorate: React.FC<Step6DecorateProps> = ({ ref, nodeIds }) => { const Step5Decorate: React.FC<Step5DecorateProps> = ({ ref, nodeIds }) => {
const { kb_id } = useAppSelector(state => state.config); const { kb_id } = useAppSelector(state => state.config);
const onSubmit = () => { const onSubmit = () => {
return getApiV1AppDetail({ return getApiV1AppDetail({
@ -60,4 +60,4 @@ const Step6Decorate: React.FC<Step6DecorateProps> = ({ ref, nodeIds }) => {
); );
}; };
export default Step6Decorate; export default Step5Decorate;

View File

@ -3,7 +3,7 @@ import { Box, Stack, Button } from '@mui/material';
import complete from '@/assets/images/init/complete.png'; import complete from '@/assets/images/init/complete.png';
import { useAppSelector } from '@/store'; import { useAppSelector } from '@/store';
const Step7Complete = () => { const Step6Complete = () => {
const { kbDetail } = useAppSelector(state => state.config); const { kbDetail } = useAppSelector(state => state.config);
const wikiUrl = useMemo(() => { const wikiUrl = useMemo(() => {
@ -56,4 +56,4 @@ const Step7Complete = () => {
); );
}; };
export default Step7Complete; export default Step6Complete;

View File

@ -1,7 +1,6 @@
export { default as Step1Model } from './Step1Model'; export { default as Step1Config } from './Step1Config';
export { default as Step2Config } from './Step2Config'; export { default as Step2Import } from './Step2Import';
export { default as Step3Import } from './Step3Import'; export { default as Step3Publish } from './Step3Publish';
export { default as Step4Publish } from './Step4Publish'; export { default as Step4Test } from './Step4Test';
export { default as Step5Test } from './Step5Test'; export { default as Step5Decorate } from './Step5Decorate';
export { default as Step6Decorate } from './Step6Decorate'; export { default as Step6Complete } from './Step6Complete';
export { default as Step7Complete } from './Step7Complete';

View File

@ -1,279 +0,0 @@
import Card from '@/components/Card';
import { Icon, message } from '@ctzhian/ui';
import {
Box,
Button,
Stack,
TextField,
Select,
MenuItem,
InputAdornment,
IconButton,
} from '@mui/material';
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
export interface AutoModelConfigRef {
getFormData: () => {
apiKey: string;
selectedModel: string;
};
}
interface AutoModelConfigProps {
showTip?: boolean;
initialApiKey?: string;
initialChatModel?: string;
onDataChange?: () => void;
}
const AutoModelConfig = forwardRef<AutoModelConfigRef, AutoModelConfigProps>(
(props, ref) => {
const {
showTip = false,
initialApiKey = '',
initialChatModel = '',
onDataChange,
} = props;
const [autoConfigApiKey, setAutoConfigApiKey] = useState(initialApiKey);
const [selectedAutoChatModel, setSelectedAutoChatModel] =
useState(initialChatModel);
const [showApiKey, setShowApiKey] = useState(false);
// 默认百智云 Chat 模型列表
const DEFAULT_BAIZHI_CLOUD_CHAT_MODELS: string[] = [
'deepseek-chat',
'deepseek-r1',
'kimi-k2-0711-preview',
'qwen-vl-max-latest',
'glm-4.5',
];
const modelList = DEFAULT_BAIZHI_CLOUD_CHAT_MODELS;
// 当从父组件接收到新的初始值时,更新状态
useEffect(() => {
if (initialApiKey) {
setAutoConfigApiKey(initialApiKey);
}
}, [initialApiKey]);
useEffect(() => {
if (initialChatModel) {
setSelectedAutoChatModel(initialChatModel);
}
}, [initialChatModel]);
// 如果没有选中模型且有可用模型,默认选择第一个
useEffect(() => {
if (modelList.length && !selectedAutoChatModel) {
setSelectedAutoChatModel(modelList[0]);
}
}, [modelList, selectedAutoChatModel]);
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
getFormData: () => ({
apiKey: autoConfigApiKey,
selectedModel: selectedAutoChatModel,
}),
}));
return (
<Stack
sx={{
flex: 1,
p: 2,
pl: 2,
pr: 0,
pt: 0,
overflow: 'hidden',
overflowY: 'auto',
}}
>
<Box>
{/* 提示信息 */}
{showTip && (
<Box
sx={{
display: 'flex',
alignItems: 'flex-start',
gap: 1,
p: 1.5,
mb: 2,
bgcolor: 'rgba(25, 118, 210, 0.08)',
borderRadius: '8px',
border: '1px solid rgba(25, 118, 210, 0.2)',
}}
>
<Icon
type='icon-info-circle'
sx={{
fontSize: 16,
color: 'primary.main',
flexShrink: 0,
mt: 0.2,
}}
/>
<Box
sx={{
fontSize: 12,
lineHeight: 1.6,
color: 'text.secondary',
}}
>
API Key PandaWiki
</Box>
</Box>
)}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: '16px',
pt: '32px',
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
fontSize: 14,
fontWeight: 'bold',
color: 'text.primary',
}}
>
<Box
sx={{
width: 4,
height: 10,
bgcolor: 'primary.main',
borderRadius: '30%',
mr: 1,
}}
/>
API Key
</Box>
<Box
component='a'
href='https://model-square.app.baizhi.cloud/token'
target='_blank'
sx={{
color: 'primary.main',
fontSize: 12,
display: 'flex',
alignItems: 'center',
gap: 0.5,
}}
>
<Icon type='icon-key' sx={{ fontSize: 14 }} />
API Key
</Box>
</Box>
<TextField
fullWidth
size='medium'
type={showApiKey ? 'text' : 'password'}
value={autoConfigApiKey}
onChange={e => {
setAutoConfigApiKey(e.target.value);
onDataChange?.();
}}
InputProps={{
endAdornment: (
<InputAdornment position='end'>
<IconButton
size='small'
onClick={() => setShowApiKey(s => !s)}
>
{showApiKey ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
sx={{
'& .MuiInputBase-root': {
borderRadius: '10px',
height: '52px',
},
}}
/>
</Box>
{!showTip && (
<Box sx={{ mt: 0 }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
fontSize: 14,
fontWeight: 'bold',
color: 'text.primary',
mb: '16px',
pt: '32px',
}}
>
<Box
sx={{
width: 4,
height: 10,
bgcolor: 'primary.main',
borderRadius: '30%',
mr: 1,
}}
/>
</Box>
<Stack direction='row' alignItems='center' gap={2}>
<Box sx={{ fontSize: 12, color: 'text.secondary', minWidth: 80 }}>
</Box>
<Select
size='medium'
displayEmpty
value={selectedAutoChatModel}
onChange={e => {
setSelectedAutoChatModel(e.target.value as string);
onDataChange?.();
}}
sx={{
width: 300,
height: '52px',
'& .MuiInputBase-root': {
borderRadius: '10px',
bgcolor: '#F8F8FA',
height: '52px',
},
'& .MuiSelect-select': {
height: '52px',
lineHeight: '52px',
padding: '0 14px',
display: 'flex',
alignItems: 'center',
},
}}
renderValue={sel =>
sel && (sel as string).length
? (sel as string)
: '请选择聊天模型'
}
>
{modelList.map((model: string) => (
<MenuItem key={model} value={model}>
{model}
</MenuItem>
))}
</Select>
</Stack>
</Box>
)}
</Stack>
);
},
);
export default AutoModelConfig;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -37,12 +37,10 @@ const RagErrorReStart = ({
const ragErrorData = const ragErrorData =
res?.filter( res?.filter(
item => item =>
item.type === 2 &&
item.rag_info?.status && item.rag_info?.status &&
[ [
ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed, ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed,
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed, ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed,
ConstsNodeRagInfoStatus.NodeRagStatusBasicPending,
].includes(item.rag_info.status), ].includes(item.rag_info.status),
) || []; ) || [];
setList(ragErrorData); setList(ragErrorData);
@ -62,14 +60,14 @@ const RagErrorReStart = ({
kb_id, kb_id,
node_ids: [...selected], node_ids: [...selected],
}).then(() => { }).then(() => {
message.success('正在学习'); message.success('正在重新学习');
setSelected([]); setSelected([]);
onClose(); onClose();
refresh(); refresh();
}); });
} else { } else {
message.error( message.error(
list.length > 0 ? '请选择要学习的文档' : '暂无需要学习的文档', list.length > 0 ? '请选择重新学习的文档' : '暂无学习失败的文档',
); );
} }
}; };
@ -85,7 +83,7 @@ const RagErrorReStart = ({
}, [selected, list]); }, [selected, list]);
return ( return (
<Modal title='学习文档' open={open} onCancel={onClose} onOk={onSubmit}> <Modal title='重新学习' open={open} onCancel={onClose} onOk={onSubmit}>
<Stack <Stack
direction='row' direction='row'
component='label' component='label'
@ -99,7 +97,7 @@ const RagErrorReStart = ({
}} }}
> >
<Box> <Box>
/
<Box <Box
component='span' component='span'
sx={{ color: 'text.tertiary', fontSize: 12, pl: 1 }} sx={{ color: 'text.tertiary', fontSize: 12, pl: 1 }}

View File

@ -25,6 +25,12 @@ import DocDelete from '../../component/DocDelete';
interface HeaderProps { interface HeaderProps {
edit: boolean; edit: boolean;
collaborativeUsers?: Array<{
id: string;
name: string;
color: string;
}>;
isSyncing?: boolean;
detail: V1NodeDetailResp; detail: V1NodeDetailResp;
updateDetail: (detail: V1NodeDetailResp) => void; updateDetail: (detail: V1NodeDetailResp) => void;
handleSave: () => void; handleSave: () => void;
@ -33,6 +39,8 @@ interface HeaderProps {
const Header = ({ const Header = ({
edit, edit,
collaborativeUsers = [],
isSyncing = false,
detail, detail,
updateDetail, updateDetail,
handleSave, handleSave,
@ -46,6 +54,10 @@ const Header = ({
const { catalogOpen, nodeDetail, setCatalogOpen } = const { catalogOpen, nodeDetail, setCatalogOpen } =
useOutletContext<WrapContext>(); useOutletContext<WrapContext>();
// const docWidth = useMemo(() => {
// return nodeDetail?.meta?.doc_width || 'full';
// }, [nodeDetail]);
const [renameOpen, setRenameOpen] = useState(false); const [renameOpen, setRenameOpen] = useState(false);
const [delOpen, setDelOpen] = useState(false); const [delOpen, setDelOpen] = useState(false);
const [publishOpen, setPublishOpen] = useState(false); const [publishOpen, setPublishOpen] = useState(false);
@ -56,6 +68,22 @@ const Header = ({
return license.edition === 2; return license.edition === 2;
}, [license]); }, [license]);
// const updateDocWidth = (doc_width: string) => {
// if (!nodeDetail) return;
// putApiV1NodeDetail({
// id: nodeDetail.id!,
// kb_id,
// doc_width,
// }).then(() => {
// updateDetail({
// meta: {
// ...nodeDetail.meta,
// doc_width,
// },
// });
// });
// };
const handlePublish = useCallback(() => { const handlePublish = useCallback(() => {
if (nodeDetail?.status === 2 && !edit) { if (nodeDetail?.status === 2 && !edit) {
message.info('当前已是最新版本!'); message.info('当前已是最新版本!');

View File

@ -39,6 +39,8 @@ const LoadingEditorWrap = () => {
> >
<Header <Header
edit={false} edit={false}
isSyncing={isSyncing}
collaborativeUsers={collaborativeUsers}
detail={{}} detail={{}}
updateDetail={() => {}} updateDetail={() => {}}
handleSave={() => {}} handleSave={() => {}}

View File

@ -40,7 +40,7 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
const { license } = useAppSelector(state => state.config); const { license } = useAppSelector(state => state.config);
const state = useLocation().state as { node?: V1NodeDetailResp }; const state = useLocation().state as { node?: V1NodeDetailResp };
const { catalogOpen, setCatalogOpen, nodeDetail, setNodeDetail, onSave } = const { catalogOpen, nodeDetail, setNodeDetail, onSave, docWidth } =
useOutletContext<WrapContext>(); useOutletContext<WrapContext>();
const storageTocOpen = localStorage.getItem('toc-open'); const storageTocOpen = localStorage.getItem('toc-open');
@ -197,41 +197,38 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
const handleExport = useCallback( const handleExport = useCallback(
async (type: string) => { async (type: string) => {
if (editorRef) { let value = editorRef?.getContent() || '';
let value = nodeDetail?.content || ''; if (isMarkdown) {
if (!isMarkdown) { value = nodeDetail?.content || '';
value = editorRef.getContent() || '';
}
if (!value) return;
const content = completeIncompleteLinks(value);
const blob = new Blob([content], { type: `text/${type}` });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${nodeDetail?.name}.${type}`;
a.click();
URL.revokeObjectURL(url);
message.success('导出成功');
} }
if (!value) return;
const content = completeIncompleteLinks(value);
const blob = new Blob([content], { type: `text/${type}` });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${nodeDetail?.name}.${type}`;
a.click();
URL.revokeObjectURL(url);
message.success('导出成功');
}, },
[editorRef, nodeDetail?.content, nodeDetail?.name, isMarkdown], [editorRef, nodeDetail?.content, nodeDetail?.name, isMarkdown],
); );
const checkIfEdited = useCallback(() => { const checkIfEdited = useCallback(() => {
if (editorRef) { let currentContent = editorRef?.getContent() || '';
let value = nodeDetail?.content || ''; if (isMarkdown) {
if (!isMarkdown) { currentContent = nodeDetail?.content || '';
value = editorRef.getContent() || '';
}
const currentSummary = summary;
const currentEmoji = nodeDetail?.meta?.emoji || '';
const hasChanges =
value !== initialStateRef.current.content ||
currentSummary !== initialStateRef.current.summary ||
currentEmoji !== initialStateRef.current.emoji;
setIsEditing(hasChanges);
} }
const currentSummary = summary;
const currentEmoji = nodeDetail?.meta?.emoji || '';
const hasChanges =
currentContent !== initialStateRef.current.content ||
currentSummary !== initialStateRef.current.summary ||
currentEmoji !== initialStateRef.current.emoji;
setIsEditing(hasChanges);
}, [ }, [
editorRef, editorRef,
summary, summary,
@ -280,18 +277,14 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
isMarkdown, isMarkdown,
]); ]);
const handleGlobalKeydown = useCallback( const handleGlobalSave = useCallback(
(event: KeyboardEvent) => { (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key === 's') { if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault(); event.preventDefault();
changeCatalogItem(); changeCatalogItem();
} }
if ((event.ctrlKey || event.metaKey) && event.key === 'b') {
event.preventDefault();
setCatalogOpen(!catalogOpen);
}
}, },
[changeCatalogItem, catalogOpen, setCatalogOpen], [changeCatalogItem],
); );
const renderEditorTitleEmojiSummary = () => { const renderEditorTitleEmojiSummary = () => {
@ -529,11 +522,11 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
}, [defaultDetail]); }, [defaultDetail]);
useEffect(() => { useEffect(() => {
document.addEventListener('keydown', handleGlobalKeydown); document.addEventListener('keydown', handleGlobalSave);
return () => { return () => {
document.removeEventListener('keydown', handleGlobalKeydown); document.removeEventListener('keydown', handleGlobalSave);
}; };
}, [handleGlobalKeydown]); }, [handleGlobalSave]);
useEffect(() => { useEffect(() => {
if (state && state.node && editorRef.editor) { if (state && state.node && editorRef.editor) {
@ -640,22 +633,17 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
detail={nodeDetail!} detail={nodeDetail!}
updateDetail={updateDetail} updateDetail={updateDetail}
handleSave={async () => { handleSave={async () => {
if (editorRef) { const content = editorRef?.getContent() || '';
let content = nodeDetail?.content || ''; updateDetail({
if (!isMarkdown) { content: content,
content = editorRef.getContent(); });
updateDetail({ await onSave(content);
content: content, initialStateRef.current = {
}); content: content,
} summary: summary,
await onSave(content); emoji: nodeDetail?.meta?.emoji || '',
initialStateRef.current = { };
content: content, setIsEditing(false);
summary: summary,
emoji: nodeDetail?.meta?.emoji || '',
};
setIsEditing(false);
}
}} }}
handleExport={handleExport} handleExport={handleExport}
/> />
@ -663,10 +651,7 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
<Toolbar editorRef={editorRef} handleAiGenerate={handleAiGenerate} /> <Toolbar editorRef={editorRef} handleAiGenerate={handleAiGenerate} />
)} )}
</Box> </Box>
<Box <Box sx={{ ...(fixedToc && { display: 'flex' }) }}>
sx={{ ...(fixedToc && { display: 'flex' }) }}
onKeyDown={event => event.stopPropagation()}
>
{isMarkdown ? ( {isMarkdown ? (
<Box <Box
sx={{ sx={{
@ -682,13 +667,12 @@ const Wrap = ({ detail: defaultDetail }: WrapProps) => {
editor={editorRef.editor} editor={editorRef.editor}
value={nodeDetail?.content || ''} value={nodeDetail?.content || ''}
onUpload={handleUpload} onUpload={handleUpload}
placeholder='请输入文档内容'
onAceChange={value => { onAceChange={value => {
updateDetail({ updateDetail({
content: value, content: value,
}); });
}} }}
height='calc(100vh - 103px)' height='calc(100vh - 360px)'
/> />
</Box> </Box>
) : ( ) : (

View File

@ -49,9 +49,9 @@ const Content = () => {
const search = searchParams.get('search') || ''; const search = searchParams.get('search') || '';
const [supportSelect, setBatchOpen] = useState(false); const [supportSelect, setBatchOpen] = useState(false);
const [ragReStartCount, setRagStartCount] = useState(0); const [ragErrorCount, setRagErrorCount] = useState(0);
const [ragIds, setRagIds] = useState<string[]>([]); const [ragErrorIds, setRagErrorIds] = useState<string[]>([]);
const [ragOpen, setRagOpen] = useState(false); const [ragErrorOpen, setRagErrorOpen] = useState(false);
const [publish, setPublish] = useState({ const [publish, setPublish] = useState({
// published: 0, // published: 0,
unpublished: 0, unpublished: 0,
@ -128,8 +128,8 @@ const Content = () => {
}; };
const handleRestudy = (item: ITreeItem) => { const handleRestudy = (item: ITreeItem) => {
setRagOpen(true); setRagErrorOpen(true);
setRagIds([item.id]); setRagErrorIds([item.id]);
}; };
const handleProperties = (item: ITreeItem) => { const handleProperties = (item: ITreeItem) => {
@ -265,25 +265,23 @@ const Content = () => {
// }, // },
] ]
: []), : []),
...(item.type === 2 && ...(item?.rag_status &&
item.rag_status &&
[ [
ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed, ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed,
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed, ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed,
ConstsNodeRagInfoStatus.NodeRagStatusBasicPending,
].includes(item.rag_status) ].includes(item.rag_status)
? [ ? [
{ {
label: label: '重新学习',
item.rag_status ===
ConstsNodeRagInfoStatus.NodeRagStatusBasicPending
? '学习文档'
: '重新学习',
key: 'restudy', key: 'restudy',
onClick: () => handleRestudy(item), onClick: () => handleRestudy(item),
}, },
] ]
: []), : []),
...(!isEditing
? [{ label: '重命名', key: 'rename', onClick: renameItem }]
: []),
{ label: '删除', key: 'delete', onClick: () => handleDelete(item) },
...(item.type === 2 ...(item.type === 2
? [ ? [
{ {
@ -293,10 +291,6 @@ const Content = () => {
}, },
] ]
: []), : []),
...(!isEditing
? [{ label: '重命名', key: 'rename', onClick: renameItem }]
: []),
{ label: '删除', key: 'delete', onClick: () => handleDelete(item) },
]; ];
}; };
@ -341,15 +335,13 @@ const Content = () => {
setPublish({ setPublish({
unpublished: res.filter(it => it.status === 1).length, unpublished: res.filter(it => it.status === 1).length,
}); });
setRagStartCount( setRagErrorCount(
res.filter( res.filter(
it => it =>
it.type === 2 &&
it.rag_info?.status && it.rag_info?.status &&
[ [
ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed, ConstsNodeRagInfoStatus.NodeRagStatusBasicFailed,
ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed, ConstsNodeRagInfoStatus.NodeRagStatusEnhanceFailed,
ConstsNodeRagInfoStatus.NodeRagStatusBasicPending,
].includes(it.rag_info.status), ].includes(it.rag_info.status),
).length, ).length,
); );
@ -429,7 +421,7 @@ const Content = () => {
</Button> </Button>
</> </>
)} )}
{ragReStartCount > 0 && ( {ragErrorCount > 0 && (
<> <>
<Box <Box
sx={{ sx={{
@ -439,16 +431,16 @@ const Content = () => {
ml: 2, ml: 2,
}} }}
> >
{ragReStartCount} {ragErrorCount}
</Box> </Box>
<Button <Button
size='small' size='small'
sx={{ minWidth: 0, p: 0, fontSize: 12 }} sx={{ minWidth: 0, p: 0, fontSize: 12 }}
onClick={() => { onClick={() => {
setRagOpen(true); setRagErrorOpen(true);
}} }}
> >
</Button> </Button>
</> </>
)} )}
@ -737,11 +729,11 @@ const Content = () => {
refresh={getData} refresh={getData}
/> />
<RagErrorReStart <RagErrorReStart
open={ragOpen} open={ragErrorOpen}
defaultSelected={ragIds} defaultSelected={ragErrorIds}
onClose={() => { onClose={() => {
setRagOpen(false); setRagErrorOpen(false);
setRagIds([]); setRagErrorIds([]);
}} }}
refresh={getData} refresh={getData}
/> />

View File

@ -15,11 +15,8 @@ import {
DomainCreateModelReq, DomainCreateModelReq,
DomainGetProviderModelListReq, DomainGetProviderModelListReq,
DomainGetProviderModelListResp, DomainGetProviderModelListResp,
DomainModelModeSetting,
DomainPWResponse, DomainPWResponse,
DomainResponse, DomainResponse,
DomainSwitchModeReq,
DomainSwitchModeResp,
DomainUpdateModelReq, DomainUpdateModelReq,
GithubComChaitinPandaWikiDomainCheckModelReq, GithubComChaitinPandaWikiDomainCheckModelReq,
GithubComChaitinPandaWikiDomainCheckModelResp, GithubComChaitinPandaWikiDomainCheckModelResp,
@ -127,32 +124,6 @@ export const getApiV1ModelList = (params: RequestParams = {}) =>
...params, ...params,
}); });
/**
* @description get current model mode setting including mode, API key and chat model
*
* @tags model
* @name GetApiV1ModelModeSetting
* @summary get model mode setting
* @request GET:/api/v1/model/mode-setting
* @response `200` `(DomainResponse & {
data?: DomainModelModeSetting,
})` OK
*/
export const getApiV1ModelModeSetting = (params: RequestParams = {}) =>
httpRequest<
DomainResponse & {
data?: DomainModelModeSetting;
}
>({
path: `/api/v1/model/mode-setting`,
method: "GET",
type: ContentType.Json,
format: "json",
...params,
});
/** /**
* @description get provider supported model list * @description get provider supported model list
* *
@ -182,33 +153,3 @@ export const postApiV1ModelProviderSupported = (
format: "json", format: "json",
...requestParams, ...requestParams,
}); });
/**
* @description switch model mode between manual and auto
*
* @tags model
* @name PostApiV1ModelSwitchMode
* @summary switch mode
* @request POST:/api/v1/model/switch-mode
* @response `200` `(DomainResponse & {
data?: DomainSwitchModeResp,
})` OK
*/
export const postApiV1ModelSwitchMode = (
request: DomainSwitchModeReq,
params: RequestParams = {},
) =>
httpRequest<
DomainResponse & {
data?: DomainSwitchModeResp;
}
>({
path: `/api/v1/model/switch-mode`,
method: "POST",
body: request,
type: ContentType.Json,
format: "json",
...params,
});

View File

@ -922,15 +922,6 @@ export interface DomainMetricsConfig {
type?: string; type?: string;
} }
export interface DomainModelModeSetting {
/** 百智云 API Key */
auto_mode_api_key?: string;
/** 自定义对话模型名称 */
chat_model?: string;
/** 模式: manual 或 auto */
mode?: string;
}
export interface DomainMoveNodeReq { export interface DomainMoveNodeReq {
id: string; id: string;
kb_id: string; kb_id: string;
@ -1204,18 +1195,6 @@ export interface DomainStatPageReq {
scene: 1 | 2 | 3 | 4; scene: 1 | 2 | 3 | 4;
} }
export interface DomainSwitchModeReq {
/** 百智云 API Key */
auto_mode_api_key?: string;
/** 自定义对话模型名称 */
chat_model?: string;
mode: "manual" | "auto";
}
export interface DomainSwitchModeResp {
message?: string;
}
export interface DomainTextConfig { export interface DomainTextConfig {
title?: string; title?: string;
type?: string; type?: string;

View File

@ -8,11 +8,19 @@ import { useEffect, useRef } from 'react';
import { useWrapContext } from '..'; import { useWrapContext } from '..';
interface HeaderProps { interface HeaderProps {
edit: boolean;
collaborativeUsers?: Array<{
id: string;
name: string;
color: string;
}>;
isSyncing?: boolean;
detail: V1NodeDetailResp; detail: V1NodeDetailResp;
updateDetail: (detail: V1NodeDetailResp) => void;
handleSave: () => void; handleSave: () => void;
} }
const Header = ({ detail, handleSave }: HeaderProps) => { const Header = ({ edit, detail, handleSave }: HeaderProps) => {
const firstLoad = useRef(true); const firstLoad = useRef(true);
const { catalogOpen, nodeDetail, setCatalogOpen, saveLoading } = const { catalogOpen, nodeDetail, setCatalogOpen, saveLoading } =

View File

@ -1,5 +1,4 @@
'use client'; 'use client';
import { V1NodeDetailResp } from '@/request/types';
import { useTiptap } from '@ctzhian/tiptap'; import { useTiptap } from '@ctzhian/tiptap';
import { Icon } from '@ctzhian/ui'; import { Icon } from '@ctzhian/ui';
import { Box, Skeleton, Stack } from '@mui/material'; import { Box, Skeleton, Stack } from '@mui/material';
@ -36,7 +35,14 @@ const LoadingEditorWrap = () => {
transition: 'left 0.3s ease-in-out', transition: 'left 0.3s ease-in-out',
}} }}
> >
<Header detail={{} as V1NodeDetailResp} handleSave={() => {}} /> <Header
edit={false}
isSyncing={isSyncing}
collaborativeUsers={collaborativeUsers}
detail={{}}
updateDetail={() => {}}
handleSave={() => {}}
/>
{editorRef.editor && <Toolbar editorRef={editorRef} />} {editorRef.editor && <Toolbar editorRef={editorRef} />}
</Box> </Box>
<Box> <Box>

View File

@ -179,14 +179,10 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
}} }}
> >
<Header <Header
edit={isEditing}
detail={nodeDetail!} detail={nodeDetail!}
updateDetail={updateDetail}
handleSave={async () => { handleSave={async () => {
if (!isMarkdown) {
const value = editorRef.getContent();
updateDetail({
content: value,
});
}
if (checkRequiredFields()) { if (checkRequiredFields()) {
setConfirmModalOpen(true); setConfirmModalOpen(true);
} }
@ -303,7 +299,6 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
ref={markdownEditorRef} ref={markdownEditorRef}
editor={editorRef.editor} editor={editorRef.editor}
value={nodeDetail?.content || defaultDetail?.content || ''} value={nodeDetail?.content || defaultDetail?.content || ''}
placeholder='请输入文档内容'
onAceChange={value => { onAceChange={value => {
updateDetail({ updateDetail({
content: value, content: value,
@ -356,17 +351,12 @@ const Wrap = ({ detail: defaultDetail = {} }: WrapProps) => {
open={confirmModalOpen} open={confirmModalOpen}
onCancel={() => setConfirmModalOpen(false)} onCancel={() => setConfirmModalOpen(false)}
onOk={async (reason: string, token: string) => { onOk={async (reason: string, token: string) => {
if (editorRef) { const value = editorRef.getContent();
let value = nodeDetail?.content || ''; updateDetail({
if (!isMarkdown) { content: value,
value = editorRef.getContent(); });
updateDetail({ await onSave(value, reason, token, isMarkdown ? 'md' : 'html');
content: value, setConfirmModalOpen(false);
});
}
await onSave(value, reason, token, isMarkdown ? 'md' : 'html');
setConfirmModalOpen(false);
}
}} }}
/> />
</> </>

View File

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

View File

@ -9,8 +9,8 @@ importers:
.: .:
dependencies: dependencies:
'@ctzhian/tiptap': '@ctzhian/tiptap':
specifier: ^1.12.21 specifier: ^1.12.20
version: 1.12.21(3f8aa6e4b731b59772b9acd58d22fc94) version: 1.12.20(3f8aa6e4b731b59772b9acd58d22fc94)
'@ctzhian/ui': '@ctzhian/ui':
specifier: ^7.0.5 specifier: ^7.0.5
version: 7.0.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/icons-material@7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/utils@7.3.3(@types/react@19.2.2)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0) version: 7.0.5(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/icons-material@7.3.4(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/utils@7.3.3(@types/react@19.2.2)(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@ -514,8 +514,8 @@ packages:
react: '>=16.9.0' react: '>=16.9.0'
react-dom: '>=16.9.0' react-dom: '>=16.9.0'
'@ctzhian/tiptap@1.12.21': '@ctzhian/tiptap@1.12.20':
resolution: {integrity: sha512-BU6ZfMbt1UzX6XeTpGvbdp2fAF/wnBz3HcjcXm8Tf4HODPrO7alIF6fPaTPOrWVnWmSCYqBEE4msYiDpAS6TXA==} resolution: {integrity: sha512-FLGgzZcvNpf1ncgPdagFaHEfqnzkWjiRw3s9tT1loyhaX+KrxQRjY86MW3qPh7qB6WIhZsfb8T1sgWLwRNN0/Q==}
peerDependencies: peerDependencies:
'@emotion/react': ^11 '@emotion/react': ^11
'@emotion/styled': ^11 '@emotion/styled': ^11
@ -5995,7 +5995,7 @@ snapshots:
- react-native - react-native
- typescript - typescript
'@ctzhian/tiptap@1.12.21(3f8aa6e4b731b59772b9acd58d22fc94)': '@ctzhian/tiptap@1.12.20(3f8aa6e4b731b59772b9acd58d22fc94)':
dependencies: dependencies:
'@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0) '@emotion/react': 11.14.0(@types/react@19.2.2)(react@19.2.0)
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0) '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@19.2.0))(@types/react@19.2.2)(react@19.2.0)