499 lines
14 KiB
Go
499 lines
14 KiB
Go
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
|
||
}
|