@@ -54,7 +54,12 @@ func (a *AIConfigApi) GetAIConfigList(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
commonResponse.OkWithData(resp, c)
|
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||||
|
List: resp.List,
|
||||||
|
Total: resp.Total,
|
||||||
|
Page: 0,
|
||||||
|
PageSize: 0,
|
||||||
|
}, "获取成功", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAIConfig
|
// UpdateAIConfig
|
||||||
|
|||||||
@@ -84,7 +84,12 @@ func (a *CharacterApi) GetCharacterList(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
commonResponse.OkWithData(resp, c)
|
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||||
|
List: resp.List,
|
||||||
|
Total: resp.Total,
|
||||||
|
Page: resp.Page,
|
||||||
|
PageSize: resp.PageSize,
|
||||||
|
}, "获取成功", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCharacterByID
|
// GetCharacterByID
|
||||||
|
|||||||
@@ -74,7 +74,12 @@ func (a *ConversationApi) GetConversationList(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
commonResponse.OkWithData(resp, c)
|
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||||
|
List: resp.List,
|
||||||
|
Total: resp.Total,
|
||||||
|
Page: resp.Page,
|
||||||
|
PageSize: resp.PageSize,
|
||||||
|
}, "获取成功", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConversationByID
|
// GetConversationByID
|
||||||
@@ -199,7 +204,12 @@ func (a *ConversationApi) GetMessageList(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
commonResponse.OkWithData(resp, c)
|
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||||
|
List: resp.List,
|
||||||
|
Total: resp.Total,
|
||||||
|
Page: resp.Page,
|
||||||
|
PageSize: resp.PageSize,
|
||||||
|
}, "获取成功", c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegenerateMessage
|
// RegenerateMessage
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ type ApiGroup struct {
|
|||||||
AIConfigApi
|
AIConfigApi
|
||||||
PresetApi
|
PresetApi
|
||||||
UploadApi
|
UploadApi
|
||||||
|
WorldbookApi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,14 +79,12 @@ func (a *PresetApi) GetPresetList(c *gin.Context) {
|
|||||||
list = append(list, response.ToPresetResponse(&preset))
|
list = append(list, response.ToPresetResponse(&preset))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := response.PresetListResponse{
|
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||||
List: list,
|
List: list,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: req.Page,
|
Page: req.Page,
|
||||||
PageSize: req.PageSize,
|
PageSize: req.PageSize,
|
||||||
}
|
}, "获取成功", c)
|
||||||
|
|
||||||
commonResponse.OkWithData(resp, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPresetByID 根据ID获取预设
|
// GetPresetByID 根据ID获取预设
|
||||||
|
|||||||
296
server/api/v1/app/worldbook.go
Normal file
296
server/api/v1/app/worldbook.go
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.echol.cn/loser/st/server/global"
|
||||||
|
"git.echol.cn/loser/st/server/model/app/request"
|
||||||
|
"git.echol.cn/loser/st/server/model/common"
|
||||||
|
commonResponse "git.echol.cn/loser/st/server/model/common/response"
|
||||||
|
"git.echol.cn/loser/st/server/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorldbookApi struct{}
|
||||||
|
|
||||||
|
// CreateWorldbook
|
||||||
|
// @Tags AppWorldbook
|
||||||
|
// @Summary 创建世界书
|
||||||
|
// @Router /app/worldbook [post]
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func (a *WorldbookApi) CreateWorldbook(c *gin.Context) {
|
||||||
|
userID := common.GetAppUserID(c)
|
||||||
|
var req request.CreateWorldbookRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.ServiceGroupApp.AppServiceGroup.WorldbookService.CreateWorldbook(userID, &req)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("创建世界书失败", zap.Error(err))
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commonResponse.OkWithData(resp, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorldbookList
|
||||||
|
// @Tags AppWorldbook
|
||||||
|
// @Summary 获取世界书列表
|
||||||
|
// @Router /app/worldbook [get]
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func (a *WorldbookApi) GetWorldbookList(c *gin.Context) {
|
||||||
|
userID := common.GetAppUserID(c)
|
||||||
|
var req request.GetWorldbookListRequest
|
||||||
|
req.Page, _ = strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
req.PageSize, _ = strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
||||||
|
req.Keyword = c.Query("keyword")
|
||||||
|
if req.Page < 1 {
|
||||||
|
req.Page = 1
|
||||||
|
}
|
||||||
|
if req.PageSize < 1 || req.PageSize > 100 {
|
||||||
|
req.PageSize = 20
|
||||||
|
}
|
||||||
|
list, total, err := service.ServiceGroupApp.AppServiceGroup.WorldbookService.GetWorldbookList(userID, &req)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("获取世界书列表失败", zap.Error(err))
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||||
|
List: list,
|
||||||
|
Total: total,
|
||||||
|
Page: req.Page,
|
||||||
|
PageSize: req.PageSize,
|
||||||
|
}, "获取成功", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorldbookByID
|
||||||
|
// @Tags AppWorldbook
|
||||||
|
// @Summary 获取世界书详情
|
||||||
|
// @Router /app/worldbook/:id [get]
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func (a *WorldbookApi) GetWorldbookByID(c *gin.Context) {
|
||||||
|
userID := common.GetAppUserID(c)
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
commonResponse.FailWithMessage("无效的世界书ID", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.ServiceGroupApp.AppServiceGroup.WorldbookService.GetWorldbookByID(userID, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("获取世界书失败", zap.Error(err))
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commonResponse.OkWithData(resp, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWorldbook
|
||||||
|
// @Tags AppWorldbook
|
||||||
|
// @Summary 更新世界书
|
||||||
|
// @Router /app/worldbook/:id [put]
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func (a *WorldbookApi) UpdateWorldbook(c *gin.Context) {
|
||||||
|
userID := common.GetAppUserID(c)
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
commonResponse.FailWithMessage("无效的世界书ID", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req request.UpdateWorldbookRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.ServiceGroupApp.AppServiceGroup.WorldbookService.UpdateWorldbook(userID, uint(id), &req); err != nil {
|
||||||
|
global.GVA_LOG.Error("更新世界书失败", zap.Error(err))
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commonResponse.OkWithMessage("更新成功", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWorldbook
|
||||||
|
// @Tags AppWorldbook
|
||||||
|
// @Summary 删除世界书
|
||||||
|
// @Router /app/worldbook/:id [delete]
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func (a *WorldbookApi) DeleteWorldbook(c *gin.Context) {
|
||||||
|
userID := common.GetAppUserID(c)
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
commonResponse.FailWithMessage("无效的世界书ID", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.ServiceGroupApp.AppServiceGroup.WorldbookService.DeleteWorldbook(userID, uint(id)); err != nil {
|
||||||
|
global.GVA_LOG.Error("删除世界书失败", zap.Error(err))
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commonResponse.OkWithMessage("删除成功", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportWorldbook
|
||||||
|
// @Tags AppWorldbook
|
||||||
|
// @Summary 导入世界书(JSON)
|
||||||
|
// @Router /app/worldbook/import [post]
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func (a *WorldbookApi) ImportWorldbook(c *gin.Context) {
|
||||||
|
userID := common.GetAppUserID(c)
|
||||||
|
file, header, err := c.Request.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
commonResponse.FailWithMessage("请上传文件", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if header.Size > 5*1024*1024 {
|
||||||
|
commonResponse.FailWithMessage("文件大小不能超过 5MB", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, header.Size)
|
||||||
|
if _, err := file.Read(buf); err != nil {
|
||||||
|
commonResponse.FailWithMessage("读取文件失败", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去掉扩展名作为名称
|
||||||
|
filename := header.Filename
|
||||||
|
for i := len(filename) - 1; i >= 0; i-- {
|
||||||
|
if filename[i] == '.' {
|
||||||
|
filename = filename[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := service.ServiceGroupApp.AppServiceGroup.WorldbookService.ImportFromJSON(userID, buf, filename)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("导入世界书失败", zap.Error(err))
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commonResponse.OkWithData(resp, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportWorldbook
|
||||||
|
// @Tags AppWorldbook
|
||||||
|
// @Summary 导出世界书为 JSON
|
||||||
|
// @Router /app/worldbook/:id/export [get]
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func (a *WorldbookApi) ExportWorldbook(c *gin.Context) {
|
||||||
|
userID := common.GetAppUserID(c)
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
commonResponse.FailWithMessage("无效的世界书ID", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, filename, err := service.ServiceGroupApp.AppServiceGroup.WorldbookService.ExportToJSON(userID, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("导出世界书失败", zap.Error(err))
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||||||
|
c.Data(http.StatusOK, "application/json", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEntry
|
||||||
|
// @Tags AppWorldbook
|
||||||
|
// @Summary 创建世界书条目
|
||||||
|
// @Router /app/worldbook/:id/entry [post]
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func (a *WorldbookApi) CreateEntry(c *gin.Context) {
|
||||||
|
userID := common.GetAppUserID(c)
|
||||||
|
worldbookID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
commonResponse.FailWithMessage("无效的世界书ID", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req request.CreateEntryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := service.ServiceGroupApp.AppServiceGroup.WorldbookService.CreateEntry(userID, uint(worldbookID), &req)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("创建世界书条目失败", zap.Error(err))
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commonResponse.OkWithData(resp, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEntryList
|
||||||
|
// @Tags AppWorldbook
|
||||||
|
// @Summary 获取世界书条目列表
|
||||||
|
// @Router /app/worldbook/:id/entries [get]
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func (a *WorldbookApi) GetEntryList(c *gin.Context) {
|
||||||
|
userID := common.GetAppUserID(c)
|
||||||
|
worldbookID, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
commonResponse.FailWithMessage("无效的世界书ID", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, total, err := service.ServiceGroupApp.AppServiceGroup.WorldbookService.GetEntryList(userID, uint(worldbookID))
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("获取条目列表失败", zap.Error(err))
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||||
|
List: list,
|
||||||
|
Total: total,
|
||||||
|
Page: 0,
|
||||||
|
PageSize: 0,
|
||||||
|
}, "获取成功", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEntry
|
||||||
|
// @Tags AppWorldbook
|
||||||
|
// @Summary 更新世界书条目
|
||||||
|
// @Router /app/worldbook/:id/entry/:entryId [put]
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func (a *WorldbookApi) UpdateEntry(c *gin.Context) {
|
||||||
|
userID := common.GetAppUserID(c)
|
||||||
|
entryID, err := strconv.ParseUint(c.Param("entryId"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
commonResponse.FailWithMessage("无效的条目ID", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req request.UpdateEntryRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.ServiceGroupApp.AppServiceGroup.WorldbookService.UpdateEntry(userID, uint(entryID), &req); err != nil {
|
||||||
|
global.GVA_LOG.Error("更新条目失败", zap.Error(err))
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commonResponse.OkWithMessage("更新成功", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEntry
|
||||||
|
// @Tags AppWorldbook
|
||||||
|
// @Summary 删除世界书条目
|
||||||
|
// @Router /app/worldbook/:id/entry/:entryId [delete]
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
func (a *WorldbookApi) DeleteEntry(c *gin.Context) {
|
||||||
|
userID := common.GetAppUserID(c)
|
||||||
|
entryID, err := strconv.ParseUint(c.Param("entryId"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
commonResponse.FailWithMessage("无效的条目ID", c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := service.ServiceGroupApp.AppServiceGroup.WorldbookService.DeleteEntry(userID, uint(entryID)); err != nil {
|
||||||
|
global.GVA_LOG.Error("删除条目失败", zap.Error(err))
|
||||||
|
commonResponse.FailWithMessage(err.Error(), c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
commonResponse.OkWithMessage("删除成功", c)
|
||||||
|
}
|
||||||
@@ -86,6 +86,8 @@ func RegisterTables() {
|
|||||||
app.Message{},
|
app.Message{},
|
||||||
app.AIConfig{},
|
app.AIConfig{},
|
||||||
app.AIPreset{},
|
app.AIPreset{},
|
||||||
|
app.Worldbook{},
|
||||||
|
app.WorldbookEntry{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("register table failed", zap.Error(err))
|
global.GVA_LOG.Error("register table failed", zap.Error(err))
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ func Routers() *gin.Engine {
|
|||||||
appRouter.InitAIConfigRouter(appGroup) // AI配置路由:/app/ai-config/*
|
appRouter.InitAIConfigRouter(appGroup) // AI配置路由:/app/ai-config/*
|
||||||
appRouter.InitPresetRouter(appGroup) // 预设路由:/app/preset/*
|
appRouter.InitPresetRouter(appGroup) // 预设路由:/app/preset/*
|
||||||
appRouter.InitUploadRouter(appGroup) // 上传路由:/app/upload/*
|
appRouter.InitUploadRouter(appGroup) // 上传路由:/app/upload/*
|
||||||
|
appRouter.InitWorldbookRouter(appGroup) // 世界书路由:/app/worldbook/*
|
||||||
}
|
}
|
||||||
|
|
||||||
//插件路由安装
|
//插件路由安装
|
||||||
|
|||||||
64
server/model/app/request/worldbook.go
Normal file
64
server/model/app/request/worldbook.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
// CreateWorldbookRequest 创建世界书请求
|
||||||
|
type CreateWorldbookRequest struct {
|
||||||
|
Name string `json:"name" binding:"required,max=100"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
IsPublic bool `json:"isPublic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWorldbookRequest 更新世界书请求
|
||||||
|
type UpdateWorldbookRequest struct {
|
||||||
|
Name *string `json:"name" binding:"omitempty,max=100"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
IsPublic *bool `json:"isPublic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorldbookListRequest 获取世界书列表请求
|
||||||
|
type GetWorldbookListRequest struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"pageSize"`
|
||||||
|
Keyword string `json:"keyword"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEntryRequest 创建世界书条目请求
|
||||||
|
type CreateEntryRequest struct {
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
Keys []string `json:"keys"`
|
||||||
|
SecondaryKeys []string `json:"secondaryKeys"`
|
||||||
|
Constant bool `json:"constant"`
|
||||||
|
Enabled *bool `json:"enabled"` // 指针以区分 false 和未传
|
||||||
|
UseRegex bool `json:"useRegex"`
|
||||||
|
CaseSensitive bool `json:"caseSensitive"`
|
||||||
|
MatchWholeWords bool `json:"matchWholeWords"`
|
||||||
|
Selective bool `json:"selective"`
|
||||||
|
SelectiveLogic int `json:"selectiveLogic"`
|
||||||
|
Position int `json:"position"`
|
||||||
|
Depth int `json:"depth"`
|
||||||
|
Order int `json:"order"`
|
||||||
|
Probability int `json:"probability"`
|
||||||
|
ScanDepth int `json:"scanDepth"`
|
||||||
|
GroupID string `json:"groupId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEntryRequest 更新世界书条目请求(所有字段可选)
|
||||||
|
type UpdateEntryRequest struct {
|
||||||
|
Comment *string `json:"comment"`
|
||||||
|
Content *string `json:"content"`
|
||||||
|
Keys []string `json:"keys"`
|
||||||
|
SecondaryKeys []string `json:"secondaryKeys"`
|
||||||
|
Constant *bool `json:"constant"`
|
||||||
|
Enabled *bool `json:"enabled"`
|
||||||
|
UseRegex *bool `json:"useRegex"`
|
||||||
|
CaseSensitive *bool `json:"caseSensitive"`
|
||||||
|
MatchWholeWords *bool `json:"matchWholeWords"`
|
||||||
|
Selective *bool `json:"selective"`
|
||||||
|
SelectiveLogic *int `json:"selectiveLogic"`
|
||||||
|
Position *int `json:"position"`
|
||||||
|
Depth *int `json:"depth"`
|
||||||
|
Order *int `json:"order"`
|
||||||
|
Probability *int `json:"probability"`
|
||||||
|
ScanDepth *int `json:"scanDepth"`
|
||||||
|
GroupID *string `json:"groupId"`
|
||||||
|
}
|
||||||
114
server/model/app/response/worldbook.go
Normal file
114
server/model/app/response/worldbook.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.echol.cn/loser/st/server/model/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WorldbookResponse 世界书响应
|
||||||
|
type WorldbookResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
UserID uint `json:"userId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
IsPublic bool `json:"isPublic"`
|
||||||
|
EntryCount int `json:"entryCount"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorldbookListResponse 世界书列表响应
|
||||||
|
type WorldbookListResponse struct {
|
||||||
|
List []WorldbookResponse `json:"list"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"pageSize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EntryResponse 世界书条目响应
|
||||||
|
type EntryResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
WorldbookID uint `json:"worldbookId"`
|
||||||
|
Comment string `json:"comment"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Keys []string `json:"keys"`
|
||||||
|
SecondaryKeys []string `json:"secondaryKeys"`
|
||||||
|
Constant bool `json:"constant"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
UseRegex bool `json:"useRegex"`
|
||||||
|
CaseSensitive bool `json:"caseSensitive"`
|
||||||
|
MatchWholeWords bool `json:"matchWholeWords"`
|
||||||
|
Selective bool `json:"selective"`
|
||||||
|
SelectiveLogic int `json:"selectiveLogic"`
|
||||||
|
Position int `json:"position"`
|
||||||
|
Depth int `json:"depth"`
|
||||||
|
Order int `json:"order"`
|
||||||
|
Probability int `json:"probability"`
|
||||||
|
ScanDepth int `json:"scanDepth"`
|
||||||
|
GroupID string `json:"groupId"`
|
||||||
|
Extensions map[string]interface{} `json:"extensions"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EntryListResponse 条目列表响应
|
||||||
|
type EntryListResponse struct {
|
||||||
|
List []EntryResponse `json:"list"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToWorldbookResponse 转换为世界书响应结构
|
||||||
|
func ToWorldbookResponse(wb *app.Worldbook) WorldbookResponse {
|
||||||
|
return WorldbookResponse{
|
||||||
|
ID: wb.ID,
|
||||||
|
UserID: wb.UserID,
|
||||||
|
Name: wb.Name,
|
||||||
|
Description: wb.Description,
|
||||||
|
IsPublic: wb.IsPublic,
|
||||||
|
EntryCount: wb.EntryCount,
|
||||||
|
CreatedAt: wb.CreatedAt,
|
||||||
|
UpdatedAt: wb.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToEntryResponse 转换为条目响应结构
|
||||||
|
func ToEntryResponse(entry *app.WorldbookEntry) EntryResponse {
|
||||||
|
var keys []string
|
||||||
|
if len(entry.Keys) > 0 {
|
||||||
|
json.Unmarshal(entry.Keys, &keys)
|
||||||
|
}
|
||||||
|
var secondaryKeys []string
|
||||||
|
if len(entry.SecondaryKeys) > 0 {
|
||||||
|
json.Unmarshal(entry.SecondaryKeys, &secondaryKeys)
|
||||||
|
}
|
||||||
|
var extensions map[string]interface{}
|
||||||
|
if len(entry.Extensions) > 0 {
|
||||||
|
json.Unmarshal(entry.Extensions, &extensions)
|
||||||
|
}
|
||||||
|
return EntryResponse{
|
||||||
|
ID: entry.ID,
|
||||||
|
WorldbookID: entry.WorldbookID,
|
||||||
|
Comment: entry.Comment,
|
||||||
|
Content: entry.Content,
|
||||||
|
Keys: keys,
|
||||||
|
SecondaryKeys: secondaryKeys,
|
||||||
|
Constant: entry.Constant,
|
||||||
|
Enabled: entry.Enabled,
|
||||||
|
UseRegex: entry.UseRegex,
|
||||||
|
CaseSensitive: entry.CaseSensitive,
|
||||||
|
MatchWholeWords: entry.MatchWholeWords,
|
||||||
|
Selective: entry.Selective,
|
||||||
|
SelectiveLogic: entry.SelectiveLogic,
|
||||||
|
Position: entry.Position,
|
||||||
|
Depth: entry.Depth,
|
||||||
|
Order: entry.Order,
|
||||||
|
Probability: entry.Probability,
|
||||||
|
ScanDepth: entry.ScanDepth,
|
||||||
|
GroupID: entry.GroupID,
|
||||||
|
Extensions: extensions,
|
||||||
|
CreatedAt: entry.CreatedAt,
|
||||||
|
UpdatedAt: entry.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
68
server/model/app/worldbook.go
Normal file
68
server/model/app/worldbook.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/datatypes"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Worldbook 世界书主表
|
||||||
|
type Worldbook struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
|
UserID uint `gorm:"index;not null" json:"userId"`
|
||||||
|
Name string `gorm:"type:varchar(100);not null" json:"name"`
|
||||||
|
Description string `gorm:"type:text" json:"description"`
|
||||||
|
IsPublic bool `gorm:"default:false" json:"isPublic"`
|
||||||
|
EntryCount int `gorm:"default:0" json:"entryCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Worldbook) TableName() string {
|
||||||
|
return "worldbooks"
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorldbookEntry 世界书条目表(完全兼容 SillyTavern 格式)
|
||||||
|
type WorldbookEntry struct {
|
||||||
|
ID uint `gorm:"primarykey" json:"id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
|
WorldbookID uint `gorm:"index;not null" json:"worldbookId"`
|
||||||
|
Comment string `gorm:"type:varchar(200)" json:"comment"` // 条目标题/备注
|
||||||
|
Content string `gorm:"type:text;not null" json:"content"` // 注入内容
|
||||||
|
|
||||||
|
// 关键词(存为 JSONB []string)
|
||||||
|
Keys datatypes.JSON `gorm:"type:jsonb" json:"keys"`
|
||||||
|
SecondaryKeys datatypes.JSON `gorm:"type:jsonb" json:"secondaryKeys"`
|
||||||
|
|
||||||
|
// 触发设置
|
||||||
|
Constant bool `gorm:"default:false" json:"constant"` // 常驻注入,无需关键词触发
|
||||||
|
Enabled bool `gorm:"default:true" json:"enabled"` // 是否启用
|
||||||
|
UseRegex bool `gorm:"default:false" json:"useRegex"` // 关键词用正则表达式
|
||||||
|
CaseSensitive bool `gorm:"default:false" json:"caseSensitive"` // 区分大小写
|
||||||
|
MatchWholeWords bool `gorm:"default:false" json:"matchWholeWords"` // 全词匹配
|
||||||
|
Selective bool `gorm:"default:false" json:"selective"` // 是否需要次要关键词
|
||||||
|
SelectiveLogic int `gorm:"default:0" json:"selectiveLogic"` // 0=AND, 1=NOT
|
||||||
|
|
||||||
|
// 注入位置与优先级
|
||||||
|
Position int `gorm:"default:1" json:"position"` // 0=系统提示词前, 1=系统提示词后, 4=指定深度
|
||||||
|
Depth int `gorm:"default:4" json:"depth"` // position=4 时生效
|
||||||
|
Order int `gorm:"default:100" json:"order"` // 同位置时的排序
|
||||||
|
|
||||||
|
// 概率与触发控制(SillyTavern 兼容字段)
|
||||||
|
Probability int `gorm:"default:100" json:"probability"` // 触发概率 0-100
|
||||||
|
ScanDepth int `gorm:"default:2" json:"scanDepth"` // 扫描最近 N 条消息(0=全部)
|
||||||
|
GroupID string `gorm:"type:varchar(100)" json:"groupId"` // 条目分组
|
||||||
|
|
||||||
|
// 扩展字段
|
||||||
|
Extensions datatypes.JSON `gorm:"type:jsonb" json:"extensions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (WorldbookEntry) TableName() string {
|
||||||
|
return "worldbook_entries"
|
||||||
|
}
|
||||||
@@ -7,4 +7,5 @@ type RouterGroup struct {
|
|||||||
AIConfigRouter
|
AIConfigRouter
|
||||||
PresetRouter
|
PresetRouter
|
||||||
UploadRouter
|
UploadRouter
|
||||||
|
WorldbookRouter
|
||||||
}
|
}
|
||||||
|
|||||||
29
server/router/app/worldbook.go
Normal file
29
server/router/app/worldbook.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
v1 "git.echol.cn/loser/st/server/api/v1"
|
||||||
|
"git.echol.cn/loser/st/server/middleware"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorldbookRouter struct{}
|
||||||
|
|
||||||
|
// InitWorldbookRouter 初始化世界书路由
|
||||||
|
func (r *WorldbookRouter) InitWorldbookRouter(Router *gin.RouterGroup) {
|
||||||
|
worldbookRouter := Router.Group("worldbook").Use(middleware.AppJWTAuth())
|
||||||
|
worldbookApi := v1.ApiGroupApp.AppApiGroup.WorldbookApi
|
||||||
|
|
||||||
|
{
|
||||||
|
worldbookRouter.POST("", worldbookApi.CreateWorldbook) // 创建世界书
|
||||||
|
worldbookRouter.GET("", worldbookApi.GetWorldbookList) // 获取世界书列表
|
||||||
|
worldbookRouter.POST("import", worldbookApi.ImportWorldbook) // 导入世界书
|
||||||
|
worldbookRouter.GET(":id", worldbookApi.GetWorldbookByID) // 获取世界书详情
|
||||||
|
worldbookRouter.PUT(":id", worldbookApi.UpdateWorldbook) // 更新世界书
|
||||||
|
worldbookRouter.DELETE(":id", worldbookApi.DeleteWorldbook) // 删除世界书
|
||||||
|
worldbookRouter.GET(":id/export", worldbookApi.ExportWorldbook) // 导出世界书
|
||||||
|
worldbookRouter.POST(":id/entry", worldbookApi.CreateEntry) // 创建条目
|
||||||
|
worldbookRouter.GET(":id/entries", worldbookApi.GetEntryList) // 获取条目列表
|
||||||
|
worldbookRouter.PUT(":id/entry/:entryId", worldbookApi.UpdateEntry) // 更新条目
|
||||||
|
worldbookRouter.DELETE(":id/entry/:entryId", worldbookApi.DeleteEntry) // 删除条目
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,5 @@ type AppServiceGroup struct {
|
|||||||
AIConfigService
|
AIConfigService
|
||||||
PresetService
|
PresetService
|
||||||
UploadService
|
UploadService
|
||||||
|
WorldbookService
|
||||||
}
|
}
|
||||||
|
|||||||
498
server/service/app/worldbook.go
Normal file
498
server/service/app/worldbook.go
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.echol.cn/loser/st/server/global"
|
||||||
|
"git.echol.cn/loser/st/server/model/app"
|
||||||
|
"git.echol.cn/loser/st/server/model/app/request"
|
||||||
|
"git.echol.cn/loser/st/server/model/app/response"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/datatypes"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorldbookService struct{}
|
||||||
|
|
||||||
|
// CreateWorldbook 创建世界书
|
||||||
|
func (s *WorldbookService) CreateWorldbook(userID uint, req *request.CreateWorldbookRequest) (*response.WorldbookResponse, error) {
|
||||||
|
wb := &app.Worldbook{
|
||||||
|
UserID: userID,
|
||||||
|
Name: req.Name,
|
||||||
|
Description: req.Description,
|
||||||
|
IsPublic: req.IsPublic,
|
||||||
|
}
|
||||||
|
if err := global.GVA_DB.Create(wb).Error; err != nil {
|
||||||
|
global.GVA_LOG.Error("创建世界书失败", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := response.ToWorldbookResponse(wb)
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorldbookList 获取世界书列表(自己的 + 公开的)
|
||||||
|
func (s *WorldbookService) GetWorldbookList(userID uint, req *request.GetWorldbookListRequest) ([]response.WorldbookResponse, int64, error) {
|
||||||
|
var worldbooks []app.Worldbook
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
db := global.GVA_DB.Model(&app.Worldbook{}).Where("user_id = ? OR is_public = ?", userID, true)
|
||||||
|
|
||||||
|
if req.Keyword != "" {
|
||||||
|
db = db.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (req.Page - 1) * req.PageSize
|
||||||
|
if err := db.Order("updated_at DESC").Offset(offset).Limit(req.PageSize).Find(&worldbooks).Error; err != nil {
|
||||||
|
global.GVA_LOG.Error("获取世界书列表失败", zap.Error(err))
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var list []response.WorldbookResponse
|
||||||
|
for i := range worldbooks {
|
||||||
|
list = append(list, response.ToWorldbookResponse(&worldbooks[i]))
|
||||||
|
}
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorldbookByID 获取世界书详情
|
||||||
|
func (s *WorldbookService) GetWorldbookByID(userID uint, id uint) (*response.WorldbookResponse, error) {
|
||||||
|
var wb app.Worldbook
|
||||||
|
if err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", id, userID, true).
|
||||||
|
First(&wb).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errors.New("世界书不存在或无权访问")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := response.ToWorldbookResponse(&wb)
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWorldbook 更新世界书
|
||||||
|
func (s *WorldbookService) UpdateWorldbook(userID uint, id uint, req *request.UpdateWorldbookRequest) error {
|
||||||
|
var wb app.Worldbook
|
||||||
|
if err := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).First(&wb).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return errors.New("世界书不存在或无权修改")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := make(map[string]interface{})
|
||||||
|
if req.Name != nil {
|
||||||
|
updates["name"] = *req.Name
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
updates["description"] = *req.Description
|
||||||
|
}
|
||||||
|
if req.IsPublic != nil {
|
||||||
|
updates["is_public"] = *req.IsPublic
|
||||||
|
}
|
||||||
|
|
||||||
|
return global.GVA_DB.Model(&wb).Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWorldbook 删除世界书(级联删除条目)
|
||||||
|
func (s *WorldbookService) DeleteWorldbook(userID uint, id uint) error {
|
||||||
|
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 删除所有条目
|
||||||
|
if err := tx.Where("worldbook_id = ?", id).Delete(&app.WorldbookEntry{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 删除世界书
|
||||||
|
result := tx.Where("id = ? AND user_id = ?", id, userID).Delete(&app.Worldbook{})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return errors.New("世界书不存在或无权删除")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEntry 创建世界书条目
|
||||||
|
func (s *WorldbookService) CreateEntry(userID uint, worldbookID uint, req *request.CreateEntryRequest) (*response.EntryResponse, error) {
|
||||||
|
// 验证世界书归属
|
||||||
|
var wb app.Worldbook
|
||||||
|
if err := global.GVA_DB.Where("id = ? AND user_id = ?", worldbookID, userID).First(&wb).Error; err != nil {
|
||||||
|
return nil, errors.New("世界书不存在或无权操作")
|
||||||
|
}
|
||||||
|
|
||||||
|
keysJSON, _ := json.Marshal(req.Keys)
|
||||||
|
secKeysJSON, _ := json.Marshal(req.SecondaryKeys)
|
||||||
|
|
||||||
|
enabled := true
|
||||||
|
if req.Enabled != nil {
|
||||||
|
enabled = *req.Enabled
|
||||||
|
}
|
||||||
|
probability := req.Probability
|
||||||
|
if probability == 0 {
|
||||||
|
probability = 100
|
||||||
|
}
|
||||||
|
order := req.Order
|
||||||
|
if order == 0 {
|
||||||
|
order = 100
|
||||||
|
}
|
||||||
|
scanDepth := req.ScanDepth
|
||||||
|
if scanDepth == 0 {
|
||||||
|
scanDepth = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &app.WorldbookEntry{
|
||||||
|
WorldbookID: worldbookID,
|
||||||
|
Comment: req.Comment,
|
||||||
|
Content: req.Content,
|
||||||
|
Keys: datatypes.JSON(keysJSON),
|
||||||
|
SecondaryKeys: datatypes.JSON(secKeysJSON),
|
||||||
|
Constant: req.Constant,
|
||||||
|
Enabled: enabled,
|
||||||
|
UseRegex: req.UseRegex,
|
||||||
|
CaseSensitive: req.CaseSensitive,
|
||||||
|
MatchWholeWords: req.MatchWholeWords,
|
||||||
|
Selective: req.Selective,
|
||||||
|
SelectiveLogic: req.SelectiveLogic,
|
||||||
|
Position: req.Position,
|
||||||
|
Depth: req.Depth,
|
||||||
|
Order: order,
|
||||||
|
Probability: probability,
|
||||||
|
ScanDepth: scanDepth,
|
||||||
|
GroupID: req.GroupID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := global.GVA_DB.Create(entry).Error; err != nil {
|
||||||
|
global.GVA_LOG.Error("创建世界书条目失败", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新世界书条目计数
|
||||||
|
global.GVA_DB.Model(&wb).Update("entry_count", gorm.Expr("entry_count + ?", 1))
|
||||||
|
|
||||||
|
resp := response.ToEntryResponse(entry)
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEntryList 获取世界书条目列表
|
||||||
|
func (s *WorldbookService) GetEntryList(userID uint, worldbookID uint) ([]response.EntryResponse, int64, error) {
|
||||||
|
// 验证访问权限
|
||||||
|
var wb app.Worldbook
|
||||||
|
if err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", worldbookID, userID, true).
|
||||||
|
First(&wb).Error; err != nil {
|
||||||
|
return nil, 0, errors.New("世界书不存在或无权访问")
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []app.WorldbookEntry
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
db := global.GVA_DB.Model(&app.WorldbookEntry{}).Where("worldbook_id = ?", worldbookID)
|
||||||
|
db.Count(&total)
|
||||||
|
|
||||||
|
if err := db.Order("`order` ASC, created_at ASC").Find(&entries).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var list []response.EntryResponse
|
||||||
|
for i := range entries {
|
||||||
|
list = append(list, response.ToEntryResponse(&entries[i]))
|
||||||
|
}
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEntry 更新世界书条目
|
||||||
|
func (s *WorldbookService) UpdateEntry(userID uint, entryID uint, req *request.UpdateEntryRequest) error {
|
||||||
|
// 查找条目并验证归属
|
||||||
|
var entry app.WorldbookEntry
|
||||||
|
if err := global.GVA_DB.First(&entry, entryID).Error; err != nil {
|
||||||
|
return errors.New("条目不存在")
|
||||||
|
}
|
||||||
|
var wb app.Worldbook
|
||||||
|
if err := global.GVA_DB.Where("id = ? AND user_id = ?", entry.WorldbookID, userID).First(&wb).Error; err != nil {
|
||||||
|
return errors.New("无权修改此条目")
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := make(map[string]interface{})
|
||||||
|
if req.Comment != nil {
|
||||||
|
updates["comment"] = *req.Comment
|
||||||
|
}
|
||||||
|
if req.Content != nil {
|
||||||
|
updates["content"] = *req.Content
|
||||||
|
}
|
||||||
|
if req.Keys != nil {
|
||||||
|
keysJSON, _ := json.Marshal(req.Keys)
|
||||||
|
updates["keys"] = datatypes.JSON(keysJSON)
|
||||||
|
}
|
||||||
|
if req.SecondaryKeys != nil {
|
||||||
|
secKeysJSON, _ := json.Marshal(req.SecondaryKeys)
|
||||||
|
updates["secondary_keys"] = datatypes.JSON(secKeysJSON)
|
||||||
|
}
|
||||||
|
if req.Constant != nil {
|
||||||
|
updates["constant"] = *req.Constant
|
||||||
|
}
|
||||||
|
if req.Enabled != nil {
|
||||||
|
updates["enabled"] = *req.Enabled
|
||||||
|
}
|
||||||
|
if req.UseRegex != nil {
|
||||||
|
updates["use_regex"] = *req.UseRegex
|
||||||
|
}
|
||||||
|
if req.CaseSensitive != nil {
|
||||||
|
updates["case_sensitive"] = *req.CaseSensitive
|
||||||
|
}
|
||||||
|
if req.MatchWholeWords != nil {
|
||||||
|
updates["match_whole_words"] = *req.MatchWholeWords
|
||||||
|
}
|
||||||
|
if req.Selective != nil {
|
||||||
|
updates["selective"] = *req.Selective
|
||||||
|
}
|
||||||
|
if req.SelectiveLogic != nil {
|
||||||
|
updates["selective_logic"] = *req.SelectiveLogic
|
||||||
|
}
|
||||||
|
if req.Position != nil {
|
||||||
|
updates["position"] = *req.Position
|
||||||
|
}
|
||||||
|
if req.Depth != nil {
|
||||||
|
updates["depth"] = *req.Depth
|
||||||
|
}
|
||||||
|
if req.Order != nil {
|
||||||
|
updates["order"] = *req.Order
|
||||||
|
}
|
||||||
|
if req.Probability != nil {
|
||||||
|
updates["probability"] = *req.Probability
|
||||||
|
}
|
||||||
|
if req.ScanDepth != nil {
|
||||||
|
updates["scan_depth"] = *req.ScanDepth
|
||||||
|
}
|
||||||
|
if req.GroupID != nil {
|
||||||
|
updates["group_id"] = *req.GroupID
|
||||||
|
}
|
||||||
|
|
||||||
|
return global.GVA_DB.Model(&entry).Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEntry 删除世界书条目
|
||||||
|
func (s *WorldbookService) DeleteEntry(userID uint, entryID uint) error {
|
||||||
|
var entry app.WorldbookEntry
|
||||||
|
if err := global.GVA_DB.First(&entry, entryID).Error; err != nil {
|
||||||
|
return errors.New("条目不存在")
|
||||||
|
}
|
||||||
|
var wb app.Worldbook
|
||||||
|
if err := global.GVA_DB.Where("id = ? AND user_id = ?", entry.WorldbookID, userID).First(&wb).Error; err != nil {
|
||||||
|
return errors.New("无权删除此条目")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := global.GVA_DB.Delete(&entry).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新条目计数
|
||||||
|
global.GVA_DB.Model(&wb).Update("entry_count", gorm.Expr("GREATEST(entry_count - 1, 0)"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportFromJSON 从 JSON 文件导入世界书(兼容 SillyTavern 格式)
|
||||||
|
func (s *WorldbookService) ImportFromJSON(userID uint, jsonData []byte, filename string) (*response.WorldbookResponse, error) {
|
||||||
|
// 尝试解析 SillyTavern 世界书格式
|
||||||
|
var stFormat map[string]interface{}
|
||||||
|
if err := json.Unmarshal(jsonData, &stFormat); err != nil {
|
||||||
|
return nil, fmt.Errorf("JSON 格式错误: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取世界书名称
|
||||||
|
name := filename
|
||||||
|
if n, ok := stFormat["name"].(string); ok && n != "" {
|
||||||
|
name = n
|
||||||
|
}
|
||||||
|
description := ""
|
||||||
|
if d, ok := stFormat["description"].(string); ok {
|
||||||
|
description = d
|
||||||
|
}
|
||||||
|
|
||||||
|
wb := &app.Worldbook{
|
||||||
|
UserID: userID,
|
||||||
|
Name: name,
|
||||||
|
Description: description,
|
||||||
|
}
|
||||||
|
if err := global.GVA_DB.Create(wb).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析条目(SillyTavern entries 格式:map[string]entry 或 []entry)
|
||||||
|
var entryCount int
|
||||||
|
if entriesRaw, ok := stFormat["entries"]; ok {
|
||||||
|
switch entries := entriesRaw.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
// SillyTavern 格式:键值对
|
||||||
|
for _, v := range entries {
|
||||||
|
if entryMap, ok := v.(map[string]interface{}); ok {
|
||||||
|
s.importEntry(wb.ID, entryMap)
|
||||||
|
entryCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
// 数组格式
|
||||||
|
for _, v := range entries {
|
||||||
|
if entryMap, ok := v.(map[string]interface{}); ok {
|
||||||
|
s.importEntry(wb.ID, entryMap)
|
||||||
|
entryCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新条目计数
|
||||||
|
global.GVA_DB.Model(wb).Update("entry_count", entryCount)
|
||||||
|
|
||||||
|
resp := response.ToWorldbookResponse(wb)
|
||||||
|
resp.EntryCount = entryCount
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// importEntry 辅助方法:从 SillyTavern 格式导入单条条目
|
||||||
|
func (s *WorldbookService) importEntry(worldbookID uint, entryMap map[string]interface{}) {
|
||||||
|
content := ""
|
||||||
|
if c, ok := entryMap["content"].(string); ok {
|
||||||
|
content = c
|
||||||
|
}
|
||||||
|
if content == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
comment := ""
|
||||||
|
if c, ok := entryMap["comment"].(string); ok {
|
||||||
|
comment = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 keys(SillyTavern 存为 []string 或 []interface{})
|
||||||
|
var keys []string
|
||||||
|
if k, ok := entryMap["key"].([]interface{}); ok {
|
||||||
|
for _, kk := range k {
|
||||||
|
if ks, ok := kk.(string); ok {
|
||||||
|
keys = append(keys, ks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if k, ok := entryMap["keys"].([]interface{}); ok {
|
||||||
|
for _, kk := range k {
|
||||||
|
if ks, ok := kk.(string); ok {
|
||||||
|
keys = append(keys, ks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keysJSON, _ := json.Marshal(keys)
|
||||||
|
|
||||||
|
var secKeys []string
|
||||||
|
if k, ok := entryMap["secondary_key"].([]interface{}); ok {
|
||||||
|
for _, kk := range k {
|
||||||
|
if ks, ok := kk.(string); ok {
|
||||||
|
secKeys = append(secKeys, ks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if k, ok := entryMap["secondaryKeys"].([]interface{}); ok {
|
||||||
|
for _, kk := range k {
|
||||||
|
if ks, ok := kk.(string); ok {
|
||||||
|
secKeys = append(secKeys, ks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
secKeysJSON, _ := json.Marshal(secKeys)
|
||||||
|
|
||||||
|
constant := false
|
||||||
|
if c, ok := entryMap["constant"].(bool); ok {
|
||||||
|
constant = c
|
||||||
|
}
|
||||||
|
enabled := true
|
||||||
|
if e, ok := entryMap["enabled"].(bool); ok {
|
||||||
|
enabled = e
|
||||||
|
} else if d, ok := entryMap["disable"].(bool); ok {
|
||||||
|
enabled = !d
|
||||||
|
}
|
||||||
|
useRegex := false
|
||||||
|
if r, ok := entryMap["use_regex"].(bool); ok {
|
||||||
|
useRegex = r
|
||||||
|
}
|
||||||
|
position := 1
|
||||||
|
if p, ok := entryMap["position"].(float64); ok {
|
||||||
|
position = int(p)
|
||||||
|
}
|
||||||
|
order := 100
|
||||||
|
if o, ok := entryMap["insertion_order"].(float64); ok {
|
||||||
|
order = int(o)
|
||||||
|
} else if o, ok := entryMap["order"].(float64); ok {
|
||||||
|
order = int(o)
|
||||||
|
}
|
||||||
|
probability := 100
|
||||||
|
if p, ok := entryMap["probability"].(float64); ok {
|
||||||
|
probability = int(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &app.WorldbookEntry{
|
||||||
|
WorldbookID: worldbookID,
|
||||||
|
Comment: comment,
|
||||||
|
Content: content,
|
||||||
|
Keys: datatypes.JSON(keysJSON),
|
||||||
|
SecondaryKeys: datatypes.JSON(secKeysJSON),
|
||||||
|
Constant: constant,
|
||||||
|
Enabled: enabled,
|
||||||
|
UseRegex: useRegex,
|
||||||
|
Position: position,
|
||||||
|
Order: order,
|
||||||
|
Probability: probability,
|
||||||
|
ScanDepth: 2,
|
||||||
|
}
|
||||||
|
global.GVA_DB.Create(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportToJSON 导出世界书为 JSON(兼容 SillyTavern 格式)
|
||||||
|
func (s *WorldbookService) ExportToJSON(userID uint, worldbookID uint) ([]byte, string, error) {
|
||||||
|
var wb app.Worldbook
|
||||||
|
if err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", worldbookID, userID, true).
|
||||||
|
First(&wb).Error; err != nil {
|
||||||
|
return nil, "", errors.New("世界书不存在或无权访问")
|
||||||
|
}
|
||||||
|
|
||||||
|
var entries []app.WorldbookEntry
|
||||||
|
global.GVA_DB.Where("worldbook_id = ?", worldbookID).Order("`order` ASC").Find(&entries)
|
||||||
|
|
||||||
|
// 构建 SillyTavern 兼容格式
|
||||||
|
entriesMap := make(map[string]interface{})
|
||||||
|
for i, entry := range entries {
|
||||||
|
var keys []string
|
||||||
|
json.Unmarshal(entry.Keys, &keys)
|
||||||
|
var secKeys []string
|
||||||
|
json.Unmarshal(entry.SecondaryKeys, &secKeys)
|
||||||
|
|
||||||
|
entriesMap[fmt.Sprintf("%d", i)] = map[string]interface{}{
|
||||||
|
"uid": entry.ID,
|
||||||
|
"key": keys,
|
||||||
|
"secondary_key": secKeys,
|
||||||
|
"comment": entry.Comment,
|
||||||
|
"content": entry.Content,
|
||||||
|
"constant": entry.Constant,
|
||||||
|
"enabled": entry.Enabled,
|
||||||
|
"use_regex": entry.UseRegex,
|
||||||
|
"case_sensitive": entry.CaseSensitive,
|
||||||
|
"match_whole_words": entry.MatchWholeWords,
|
||||||
|
"selective": entry.Selective,
|
||||||
|
"selectiveLogic": entry.SelectiveLogic,
|
||||||
|
"position": entry.Position,
|
||||||
|
"depth": entry.Depth,
|
||||||
|
"insertion_order": entry.Order,
|
||||||
|
"probability": entry.Probability,
|
||||||
|
"scanDepth": entry.ScanDepth,
|
||||||
|
"group": entry.GroupID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exportData := map[string]interface{}{
|
||||||
|
"name": wb.Name,
|
||||||
|
"description": wb.Description,
|
||||||
|
"entries": entriesMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(exportData, "", " ")
|
||||||
|
return data, wb.Name + ".json", err
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import ForgotPasswordPage from './pages/ForgotPasswordPage'
|
|||||||
import ProfilePage from './pages/ProfilePage'
|
import ProfilePage from './pages/ProfilePage'
|
||||||
import CharacterManagePage from './pages/CharacterManagePage'
|
import CharacterManagePage from './pages/CharacterManagePage'
|
||||||
import PresetManagePage from './pages/PresetManagePage'
|
import PresetManagePage from './pages/PresetManagePage'
|
||||||
|
import WorldbookManagePage from './pages/WorldbookManagePage'
|
||||||
import AdminPage from './pages/AdminPage'
|
import AdminPage from './pages/AdminPage'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -27,6 +28,7 @@ function App() {
|
|||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
<Route path="/characters" element={<CharacterManagePage />} />
|
<Route path="/characters" element={<CharacterManagePage />} />
|
||||||
<Route path="/presets" element={<PresetManagePage />} />
|
<Route path="/presets" element={<PresetManagePage />} />
|
||||||
|
<Route path="/worldbooks" element={<WorldbookManagePage />} />
|
||||||
<Route path="/admin" element={<AdminPage />} />
|
<Route path="/admin" element={<AdminPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
164
web-app/src/api/worldbook.ts
Normal file
164
web-app/src/api/worldbook.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import client from './client';
|
||||||
|
|
||||||
|
export interface Worldbook {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
isPublic: boolean;
|
||||||
|
entryCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorldbookEntry {
|
||||||
|
id: number;
|
||||||
|
worldbookId: number;
|
||||||
|
comment: string;
|
||||||
|
content: string;
|
||||||
|
keys: string[];
|
||||||
|
secondaryKeys: string[];
|
||||||
|
constant: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
useRegex: boolean;
|
||||||
|
caseSensitive: boolean;
|
||||||
|
matchWholeWords: boolean;
|
||||||
|
selective: boolean;
|
||||||
|
selectiveLogic: number;
|
||||||
|
position: number;
|
||||||
|
depth: number;
|
||||||
|
order: number;
|
||||||
|
probability: number;
|
||||||
|
scanDepth: number;
|
||||||
|
groupId: string;
|
||||||
|
extensions?: Record<string, any>;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateWorldbookRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
isPublic?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateWorldbookRequest {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
isPublic?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateEntryRequest {
|
||||||
|
comment?: string;
|
||||||
|
content: string;
|
||||||
|
keys?: string[];
|
||||||
|
secondaryKeys?: string[];
|
||||||
|
constant?: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
|
useRegex?: boolean;
|
||||||
|
caseSensitive?: boolean;
|
||||||
|
matchWholeWords?: boolean;
|
||||||
|
selective?: boolean;
|
||||||
|
selectiveLogic?: number;
|
||||||
|
position?: number;
|
||||||
|
depth?: number;
|
||||||
|
order?: number;
|
||||||
|
probability?: number;
|
||||||
|
scanDepth?: number;
|
||||||
|
groupId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateEntryRequest {
|
||||||
|
comment?: string;
|
||||||
|
content?: string;
|
||||||
|
keys?: string[];
|
||||||
|
secondaryKeys?: string[];
|
||||||
|
constant?: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
|
useRegex?: boolean;
|
||||||
|
caseSensitive?: boolean;
|
||||||
|
matchWholeWords?: boolean;
|
||||||
|
selective?: boolean;
|
||||||
|
selectiveLogic?: number;
|
||||||
|
position?: number;
|
||||||
|
depth?: number;
|
||||||
|
order?: number;
|
||||||
|
probability?: number;
|
||||||
|
scanDepth?: number;
|
||||||
|
groupId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建世界书
|
||||||
|
export const createWorldbook = (data: CreateWorldbookRequest) => {
|
||||||
|
return client.post<Worldbook>('/app/worldbook', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取世界书列表
|
||||||
|
export const getWorldbookList = (params: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
keyword?: string;
|
||||||
|
}) => {
|
||||||
|
return client.get<{
|
||||||
|
list: Worldbook[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}>('/app/worldbook', { params });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取世界书详情
|
||||||
|
export const getWorldbookById = (id: number) => {
|
||||||
|
return client.get<Worldbook>(`/app/worldbook/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新世界书
|
||||||
|
export const updateWorldbook = (id: number, data: UpdateWorldbookRequest) => {
|
||||||
|
return client.put(`/app/worldbook/${id}`, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除世界书
|
||||||
|
export const deleteWorldbook = (id: number) => {
|
||||||
|
return client.delete(`/app/worldbook/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导入世界书
|
||||||
|
export const importWorldbook = (file: File) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
return client.post<Worldbook>('/app/worldbook/import', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出世界书
|
||||||
|
export const exportWorldbook = (id: number) => {
|
||||||
|
return client.get(`/app/worldbook/${id}/export`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建条目
|
||||||
|
export const createEntry = (worldbookId: number, data: CreateEntryRequest) => {
|
||||||
|
return client.post<WorldbookEntry>(`/app/worldbook/${worldbookId}/entry`, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取条目列表
|
||||||
|
export const getEntryList = (worldbookId: number) => {
|
||||||
|
return client.get<{
|
||||||
|
list: WorldbookEntry[];
|
||||||
|
total: number;
|
||||||
|
}>(`/app/worldbook/${worldbookId}/entries`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新条目
|
||||||
|
export const updateEntry = (worldbookId: number, entryId: number, data: UpdateEntryRequest) => {
|
||||||
|
return client.put(`/app/worldbook/${worldbookId}/entry/${entryId}`, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除条目
|
||||||
|
export const deleteEntry = (worldbookId: number, entryId: number) => {
|
||||||
|
return client.delete(`/app/worldbook/${worldbookId}/entry/${entryId}`);
|
||||||
|
};
|
||||||
613
web-app/src/pages/WorldbookManagePage.tsx
Normal file
613
web-app/src/pages/WorldbookManagePage.tsx
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
getWorldbookList,
|
||||||
|
createWorldbook,
|
||||||
|
updateWorldbook,
|
||||||
|
deleteWorldbook,
|
||||||
|
importWorldbook,
|
||||||
|
exportWorldbook,
|
||||||
|
getEntryList,
|
||||||
|
createEntry,
|
||||||
|
updateEntry,
|
||||||
|
deleteEntry,
|
||||||
|
type Worldbook,
|
||||||
|
type WorldbookEntry,
|
||||||
|
type CreateWorldbookRequest,
|
||||||
|
type CreateEntryRequest,
|
||||||
|
} from '../api/worldbook';
|
||||||
|
|
||||||
|
const WorldbookManagePage: React.FC = () => {
|
||||||
|
const [worldbooks, setWorldbooks] = useState<Worldbook[]>([]);
|
||||||
|
const [selectedWorldbook, setSelectedWorldbook] = useState<Worldbook | null>(null);
|
||||||
|
const [entries, setEntries] = useState<WorldbookEntry[]>([]);
|
||||||
|
const [selectedEntry, setSelectedEntry] = useState<WorldbookEntry | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [showEntryModal, setShowEntryModal] = useState(false);
|
||||||
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
|
|
||||||
|
// 表单状态
|
||||||
|
const [worldbookForm, setWorldbookForm] = useState<CreateWorldbookRequest>({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
isPublic: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [entryForm, setEntryForm] = useState<CreateEntryRequest>({
|
||||||
|
comment: '',
|
||||||
|
content: '',
|
||||||
|
keys: [],
|
||||||
|
secondaryKeys: [],
|
||||||
|
constant: false,
|
||||||
|
enabled: true,
|
||||||
|
useRegex: false,
|
||||||
|
caseSensitive: false,
|
||||||
|
matchWholeWords: false,
|
||||||
|
selective: false,
|
||||||
|
selectiveLogic: 0,
|
||||||
|
position: 1,
|
||||||
|
depth: 4,
|
||||||
|
order: 100,
|
||||||
|
probability: 100,
|
||||||
|
scanDepth: 2,
|
||||||
|
groupId: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [keyInput, setKeyInput] = useState('');
|
||||||
|
const [secondaryKeyInput, setSecondaryKeyInput] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadWorldbooks();
|
||||||
|
}, [searchKeyword]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedWorldbook) {
|
||||||
|
loadEntries(selectedWorldbook.id);
|
||||||
|
}
|
||||||
|
}, [selectedWorldbook]);
|
||||||
|
|
||||||
|
const loadWorldbooks = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await getWorldbookList({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
keyword: searchKeyword,
|
||||||
|
});
|
||||||
|
setWorldbooks(response.data.list || []);
|
||||||
|
} catch (error: any) {
|
||||||
|
alert('加载世界书列表失败: ' + (error.response?.data?.msg || error.message));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadEntries = async (worldbookId: number) => {
|
||||||
|
try {
|
||||||
|
const response = await getEntryList(worldbookId);
|
||||||
|
setEntries(response.data.list || []);
|
||||||
|
} catch (error: any) {
|
||||||
|
alert('加载条目列表失败: ' + (error.response?.data?.msg || error.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateWorldbook = async () => {
|
||||||
|
if (!worldbookForm.name.trim()) {
|
||||||
|
alert('请输入世界书名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await createWorldbook(worldbookForm);
|
||||||
|
alert('创建成功');
|
||||||
|
setShowCreateModal(false);
|
||||||
|
setWorldbookForm({ name: '', description: '', isPublic: false });
|
||||||
|
loadWorldbooks();
|
||||||
|
} catch (error: any) {
|
||||||
|
alert('创建失败: ' + (error.response?.data?.msg || error.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteWorldbook = async (id: number) => {
|
||||||
|
if (!confirm('确定要删除这个世界书吗?所有条目也会被删除。')) return;
|
||||||
|
try {
|
||||||
|
await deleteWorldbook(id);
|
||||||
|
alert('删除成功');
|
||||||
|
if (selectedWorldbook?.id === id) {
|
||||||
|
setSelectedWorldbook(null);
|
||||||
|
setEntries([]);
|
||||||
|
}
|
||||||
|
loadWorldbooks();
|
||||||
|
} catch (error: any) {
|
||||||
|
alert('删除失败: ' + (error.response?.data?.msg || error.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await importWorldbook(file);
|
||||||
|
alert('导入成功');
|
||||||
|
loadWorldbooks();
|
||||||
|
} catch (error: any) {
|
||||||
|
alert('导入失败: ' + (error.response?.data?.msg || error.message));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const response = await exportWorldbook(id);
|
||||||
|
const blob = new Blob([response.data], { type: 'application/json' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `worldbook_${id}.json`;
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (error: any) {
|
||||||
|
alert('导出失败: ' + (error.response?.data?.msg || error.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveEntry = async () => {
|
||||||
|
if (!selectedWorldbook) return;
|
||||||
|
if (!entryForm.content.trim()) {
|
||||||
|
alert('请输入条目内容');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (selectedEntry) {
|
||||||
|
await updateEntry(selectedWorldbook.id, selectedEntry.id, entryForm);
|
||||||
|
alert('更新成功');
|
||||||
|
} else {
|
||||||
|
await createEntry(selectedWorldbook.id, entryForm);
|
||||||
|
alert('创建成功');
|
||||||
|
}
|
||||||
|
setShowEntryModal(false);
|
||||||
|
resetEntryForm();
|
||||||
|
loadEntries(selectedWorldbook.id);
|
||||||
|
} catch (error: any) {
|
||||||
|
alert('保存失败: ' + (error.response?.data?.msg || error.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteEntry = async (entryId: number) => {
|
||||||
|
if (!selectedWorldbook) return;
|
||||||
|
if (!confirm('确定要删除这个条目吗?')) return;
|
||||||
|
try {
|
||||||
|
await deleteEntry(selectedWorldbook.id, entryId);
|
||||||
|
alert('删除成功');
|
||||||
|
loadEntries(selectedWorldbook.id);
|
||||||
|
} catch (error: any) {
|
||||||
|
alert('删除失败: ' + (error.response?.data?.msg || error.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEntryModal = (entry?: WorldbookEntry) => {
|
||||||
|
if (entry) {
|
||||||
|
setSelectedEntry(entry);
|
||||||
|
setEntryForm({
|
||||||
|
comment: entry.comment,
|
||||||
|
content: entry.content,
|
||||||
|
keys: entry.keys,
|
||||||
|
secondaryKeys: entry.secondaryKeys,
|
||||||
|
constant: entry.constant,
|
||||||
|
enabled: entry.enabled,
|
||||||
|
useRegex: entry.useRegex,
|
||||||
|
caseSensitive: entry.caseSensitive,
|
||||||
|
matchWholeWords: entry.matchWholeWords,
|
||||||
|
selective: entry.selective,
|
||||||
|
selectiveLogic: entry.selectiveLogic,
|
||||||
|
position: entry.position,
|
||||||
|
depth: entry.depth,
|
||||||
|
order: entry.order,
|
||||||
|
probability: entry.probability,
|
||||||
|
scanDepth: entry.scanDepth,
|
||||||
|
groupId: entry.groupId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resetEntryForm();
|
||||||
|
}
|
||||||
|
setShowEntryModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetEntryForm = () => {
|
||||||
|
setSelectedEntry(null);
|
||||||
|
setEntryForm({
|
||||||
|
comment: '',
|
||||||
|
content: '',
|
||||||
|
keys: [],
|
||||||
|
secondaryKeys: [],
|
||||||
|
constant: false,
|
||||||
|
enabled: true,
|
||||||
|
useRegex: false,
|
||||||
|
caseSensitive: false,
|
||||||
|
matchWholeWords: false,
|
||||||
|
selective: false,
|
||||||
|
selectiveLogic: 0,
|
||||||
|
position: 1,
|
||||||
|
depth: 4,
|
||||||
|
order: 100,
|
||||||
|
probability: 100,
|
||||||
|
scanDepth: 2,
|
||||||
|
groupId: '',
|
||||||
|
});
|
||||||
|
setKeyInput('');
|
||||||
|
setSecondaryKeyInput('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const addKey = () => {
|
||||||
|
if (keyInput.trim()) {
|
||||||
|
setEntryForm({
|
||||||
|
...entryForm,
|
||||||
|
keys: [...(entryForm.keys || []), keyInput.trim()],
|
||||||
|
});
|
||||||
|
setKeyInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeKey = (index: number) => {
|
||||||
|
setEntryForm({
|
||||||
|
...entryForm,
|
||||||
|
keys: entryForm.keys?.filter((_, i) => i !== index),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSecondaryKey = () => {
|
||||||
|
if (secondaryKeyInput.trim()) {
|
||||||
|
setEntryForm({
|
||||||
|
...entryForm,
|
||||||
|
secondaryKeys: [...(entryForm.secondaryKeys || []), secondaryKeyInput.trim()],
|
||||||
|
});
|
||||||
|
setSecondaryKeyInput('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSecondaryKey = (index: number) => {
|
||||||
|
setEntryForm({
|
||||||
|
...entryForm,
|
||||||
|
secondaryKeys: entryForm.secondaryKeys?.filter((_, i) => i !== index),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h1 className="text-3xl font-bold text-white">世界书管理</h1>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<label className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg cursor-pointer transition-colors">
|
||||||
|
导入世界书
|
||||||
|
<input type="file" accept=".json" onChange={handleImport} className="hidden" />
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
创建世界书
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* 世界书列表 */}
|
||||||
|
<div className="lg:col-span-1 bg-white/10 backdrop-blur-md rounded-xl p-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索世界书..."
|
||||||
|
value={searchKeyword}
|
||||||
|
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 mb-4"
|
||||||
|
/>
|
||||||
|
<div className="space-y-2 max-h-[calc(100vh-250px)] overflow-y-auto">
|
||||||
|
{worldbooks.map((wb) => (
|
||||||
|
<div
|
||||||
|
key={wb.id}
|
||||||
|
onClick={() => setSelectedWorldbook(wb)}
|
||||||
|
className={`p-3 rounded-lg cursor-pointer transition-colors ${
|
||||||
|
selectedWorldbook?.id === wb.id
|
||||||
|
? 'bg-purple-600 text-white'
|
||||||
|
: 'bg-white/5 hover:bg-white/10 text-white/80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="font-medium">{wb.name}</div>
|
||||||
|
<div className="text-sm opacity-70">{wb.entryCount} 条目</div>
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleExport(wb.id);
|
||||||
|
}}
|
||||||
|
className="text-xs px-2 py-1 bg-blue-500 hover:bg-blue-600 rounded"
|
||||||
|
>
|
||||||
|
导出
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteWorldbook(wb.id);
|
||||||
|
}}
|
||||||
|
className="text-xs px-2 py-1 bg-red-500 hover:bg-red-600 rounded"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 条目列表 */}
|
||||||
|
<div className="lg:col-span-2 bg-white/10 backdrop-blur-md rounded-xl p-4">
|
||||||
|
{selectedWorldbook ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-xl font-bold text-white">{selectedWorldbook.name}</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => openEntryModal()}
|
||||||
|
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
添加条目
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3 max-h-[calc(100vh-250px)] overflow-y-auto">
|
||||||
|
{entries.map((entry) => (
|
||||||
|
<div key={entry.id} className="bg-white/5 rounded-lg p-4">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-white">
|
||||||
|
{entry.comment || '未命名条目'}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-white/60 mt-1">
|
||||||
|
关键词: {entry.keys.join(', ') || '无'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => openEntryModal(entry)}
|
||||||
|
className="text-xs px-3 py-1 bg-blue-500 hover:bg-blue-600 text-white rounded"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteEntry(entry.id)}
|
||||||
|
className="text-xs px-3 py-1 bg-red-500 hover:bg-red-600 text-white rounded"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-white/70 line-clamp-2">{entry.content}</div>
|
||||||
|
<div className="flex gap-2 mt-2 text-xs text-white/50">
|
||||||
|
{entry.constant && <span className="px-2 py-1 bg-yellow-500/20 rounded">常驻</span>}
|
||||||
|
{!entry.enabled && <span className="px-2 py-1 bg-red-500/20 rounded">已禁用</span>}
|
||||||
|
{entry.useRegex && <span className="px-2 py-1 bg-blue-500/20 rounded">正则</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full text-white/50">
|
||||||
|
请选择一个世界书
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 创建世界书模态框 */}
|
||||||
|
{showCreateModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6 w-full max-w-md">
|
||||||
|
<h2 className="text-xl font-bold text-white mb-4">创建世界书</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="世界书名称"
|
||||||
|
value={worldbookForm.name}
|
||||||
|
onChange={(e) => setWorldbookForm({ ...worldbookForm, name: e.target.value })}
|
||||||
|
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
placeholder="描述(可选)"
|
||||||
|
value={worldbookForm.description}
|
||||||
|
onChange={(e) => setWorldbookForm({ ...worldbookForm, description: e.target.value })}
|
||||||
|
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white h-24"
|
||||||
|
/>
|
||||||
|
<label className="flex items-center text-white">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={worldbookForm.isPublic}
|
||||||
|
onChange={(e) => setWorldbookForm({ ...worldbookForm, isPublic: e.target.checked })}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
公开世界书
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={handleCreateWorldbook}
|
||||||
|
className="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg"
|
||||||
|
>
|
||||||
|
创建
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
className="flex-1 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 条目编辑模态框 */}
|
||||||
|
{showEntryModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-slate-800 rounded-xl p-6 w-full max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<h2 className="text-xl font-bold text-white mb-4">
|
||||||
|
{selectedEntry ? '编辑条目' : '创建条目'}
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="条目标题/备注"
|
||||||
|
value={entryForm.comment}
|
||||||
|
onChange={(e) => setEntryForm({ ...entryForm, comment: e.target.value })}
|
||||||
|
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
placeholder="条目内容 *"
|
||||||
|
value={entryForm.content}
|
||||||
|
onChange={(e) => setEntryForm({ ...entryForm, content: e.target.value })}
|
||||||
|
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white h-32"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 关键词 */}
|
||||||
|
<div>
|
||||||
|
<label className="text-white text-sm mb-2 block">主关键词</label>
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="输入关键词后按回车"
|
||||||
|
value={keyInput}
|
||||||
|
onChange={(e) => setKeyInput(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && addKey()}
|
||||||
|
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||||
|
/>
|
||||||
|
<button onClick={addKey} className="px-4 py-2 bg-blue-600 rounded-lg text-white">
|
||||||
|
添加
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{entryForm.keys?.map((key, index) => (
|
||||||
|
<span key={index} className="px-3 py-1 bg-blue-500/20 text-white rounded-full text-sm flex items-center gap-2">
|
||||||
|
{key}
|
||||||
|
<button onClick={() => removeKey(index)} className="text-red-400 hover:text-red-300">×</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 次要关键词 */}
|
||||||
|
<div>
|
||||||
|
<label className="text-white text-sm mb-2 block">次要关键词(可选)</label>
|
||||||
|
<div className="flex gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="输入次要关键词后按回车"
|
||||||
|
value={secondaryKeyInput}
|
||||||
|
onChange={(e) => setSecondaryKeyInput(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && addSecondaryKey()}
|
||||||
|
className="flex-1 px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||||
|
/>
|
||||||
|
<button onClick={addSecondaryKey} className="px-4 py-2 bg-blue-600 rounded-lg text-white">
|
||||||
|
添加
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{entryForm.secondaryKeys?.map((key, index) => (
|
||||||
|
<span key={index} className="px-3 py-1 bg-purple-500/20 text-white rounded-full text-sm flex items-center gap-2">
|
||||||
|
{key}
|
||||||
|
<button onClick={() => removeSecondaryKey(index)} className="text-red-400 hover:text-red-300">×</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 选项 */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<label className="flex items-center text-white">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={entryForm.constant}
|
||||||
|
onChange={(e) => setEntryForm({ ...entryForm, constant: e.target.checked })}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
常驻注入
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center text-white">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={entryForm.enabled}
|
||||||
|
onChange={(e) => setEntryForm({ ...entryForm, enabled: e.target.checked })}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
启用
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center text-white">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={entryForm.useRegex}
|
||||||
|
onChange={(e) => setEntryForm({ ...entryForm, useRegex: e.target.checked })}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
使用正则
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center text-white">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={entryForm.caseSensitive}
|
||||||
|
onChange={(e) => setEntryForm({ ...entryForm, caseSensitive: e.target.checked })}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
区分大小写
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 高级选项 */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-white text-sm mb-1 block">触发概率 (%)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={entryForm.probability}
|
||||||
|
onChange={(e) => setEntryForm({ ...entryForm, probability: parseInt(e.target.value) })}
|
||||||
|
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-white text-sm mb-1 block">扫描深度</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={entryForm.scanDepth}
|
||||||
|
onChange={(e) => setEntryForm({ ...entryForm, scanDepth: parseInt(e.target.value) })}
|
||||||
|
className="w-full px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={handleSaveEntry}
|
||||||
|
className="flex-1 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowEntryModal(false);
|
||||||
|
resetEntryForm();
|
||||||
|
}}
|
||||||
|
className="flex-1 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorldbookManagePage;
|
||||||
Reference in New Issue
Block a user