🎨 新增世界书模块,将原有的世界书分离

Signed-off-by: Echo <1711788888@qq.com>
This commit is contained in:
2026-02-27 23:15:30 +08:00
parent 689e8af3df
commit 032d0ccdf0
18 changed files with 1880 additions and 8 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -7,4 +7,5 @@ type ApiGroup struct {
AIConfigApi
PresetApi
UploadApi
WorldbookApi
}

View File

@@ -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获取预设

View 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)
}

View File

@@ -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))

View File

@@ -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/*
}
//插件路由安装

View 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"`
}

View 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,
}
}

View 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"
}

View File

@@ -7,4 +7,5 @@ type RouterGroup struct {
AIConfigRouter
PresetRouter
UploadRouter
WorldbookRouter
}

View 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) // 删除条目
}
}

View File

@@ -7,4 +7,5 @@ type AppServiceGroup struct {
AIConfigService
PresetService
UploadService
WorldbookService
}

View 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
}
// 解析 keysSillyTavern 存为 []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
}