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

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

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