Files
st-react/server/service/app/worldbook.go
2026-02-27 23:15:30 +08:00

499 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}