@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user