@@ -54,7 +54,12 @@ func (a *AIConfigApi) GetAIConfigList(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
commonResponse.OkWithData(resp, c)
|
||||
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||
List: resp.List,
|
||||
Total: resp.Total,
|
||||
Page: 0,
|
||||
PageSize: 0,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// UpdateAIConfig
|
||||
|
||||
@@ -84,7 +84,12 @@ func (a *CharacterApi) GetCharacterList(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
commonResponse.OkWithData(resp, c)
|
||||
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||
List: resp.List,
|
||||
Total: resp.Total,
|
||||
Page: resp.Page,
|
||||
PageSize: resp.PageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetCharacterByID
|
||||
|
||||
@@ -74,7 +74,12 @@ func (a *ConversationApi) GetConversationList(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
commonResponse.OkWithData(resp, c)
|
||||
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||
List: resp.List,
|
||||
Total: resp.Total,
|
||||
Page: resp.Page,
|
||||
PageSize: resp.PageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// GetConversationByID
|
||||
@@ -199,7 +204,12 @@ func (a *ConversationApi) GetMessageList(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
commonResponse.OkWithData(resp, c)
|
||||
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||
List: resp.List,
|
||||
Total: resp.Total,
|
||||
Page: resp.Page,
|
||||
PageSize: resp.PageSize,
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// RegenerateMessage
|
||||
|
||||
@@ -7,4 +7,5 @@ type ApiGroup struct {
|
||||
AIConfigApi
|
||||
PresetApi
|
||||
UploadApi
|
||||
WorldbookApi
|
||||
}
|
||||
|
||||
@@ -79,14 +79,12 @@ func (a *PresetApi) GetPresetList(c *gin.Context) {
|
||||
list = append(list, response.ToPresetResponse(&preset))
|
||||
}
|
||||
|
||||
resp := response.PresetListResponse{
|
||||
commonResponse.OkWithDetailed(commonResponse.PageResult{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
}
|
||||
|
||||
commonResponse.OkWithData(resp, c)
|
||||
}, "获取成功", c)
|
||||
}
|
||||
|
||||
// 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.AIConfig{},
|
||||
app.AIPreset{},
|
||||
app.Worldbook{},
|
||||
app.WorldbookEntry{},
|
||||
)
|
||||
if err != nil {
|
||||
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.InitPresetRouter(appGroup) // 预设路由:/app/preset/*
|
||||
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
|
||||
PresetRouter
|
||||
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
|
||||
PresetService
|
||||
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 CharacterManagePage from './pages/CharacterManagePage'
|
||||
import PresetManagePage from './pages/PresetManagePage'
|
||||
import WorldbookManagePage from './pages/WorldbookManagePage'
|
||||
import AdminPage from './pages/AdminPage'
|
||||
|
||||
function App() {
|
||||
@@ -27,6 +28,7 @@ function App() {
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="/characters" element={<CharacterManagePage />} />
|
||||
<Route path="/presets" element={<PresetManagePage />} />
|
||||
<Route path="/worldbooks" element={<WorldbookManagePage />} />
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
</Routes>
|
||||
</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