🎉 初始化项目

Signed-off-by: Echo <1711788888@qq.com>
This commit is contained in:
2026-02-27 21:52:00 +08:00
commit f4e166c5ee
482 changed files with 55079 additions and 0 deletions

View File

@@ -0,0 +1,316 @@
package app
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"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"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type AIConfigService struct{}
// CreateAIConfig 创建AI配置
func (s *AIConfigService) CreateAIConfig(req *request.CreateAIConfigRequest) (*response.AIConfigResponse, error) {
// 序列化 JSON 字段
settingsJSON, _ := json.Marshal(req.Settings)
config := app.AIConfig{
Name: req.Name,
Provider: req.Provider,
BaseURL: req.BaseURL,
APIKey: req.APIKey,
DefaultModel: req.DefaultModel,
Settings: datatypes.JSON(settingsJSON),
Models: datatypes.JSON("[]"),
IsActive: true,
IsDefault: false,
}
err := global.GVA_DB.Create(&config).Error
if err != nil {
return nil, err
}
resp := response.ToAIConfigResponse(&config)
return &resp, nil
}
// GetAIConfigList 获取AI配置列表
func (s *AIConfigService) GetAIConfigList() (*response.AIConfigListResponse, error) {
var configs []app.AIConfig
var total int64
db := global.GVA_DB.Model(&app.AIConfig{})
err := db.Count(&total).Error
if err != nil {
return nil, err
}
err = db.Order("is_default DESC, created_at DESC").Find(&configs).Error
if err != nil {
return nil, err
}
list := make([]response.AIConfigResponse, len(configs))
for i, config := range configs {
list[i] = response.ToAIConfigResponse(&config)
}
return &response.AIConfigListResponse{
List: list,
Total: total,
}, nil
}
// UpdateAIConfig 更新AI配置
func (s *AIConfigService) UpdateAIConfig(id uint, req *request.UpdateAIConfigRequest) error {
var config app.AIConfig
err := global.GVA_DB.First(&config, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("配置不存在")
}
return err
}
updates := map[string]interface{}{}
if req.Name != "" {
updates["name"] = req.Name
}
if req.BaseURL != "" {
updates["base_url"] = req.BaseURL
}
// 只有当 API Key 不是脱敏格式时才更新
// 脱敏格式: xxxx****xxxx
if req.APIKey != "" && !isMaskedAPIKey(req.APIKey) {
updates["api_key"] = req.APIKey
}
if req.DefaultModel != "" {
updates["default_model"] = req.DefaultModel
}
if req.Settings != nil {
settingsJSON, _ := json.Marshal(req.Settings)
updates["settings"] = datatypes.JSON(settingsJSON)
}
if req.IsActive != nil {
updates["is_active"] = *req.IsActive
}
if req.IsDefault != nil && *req.IsDefault {
// 如果设置为默认,先取消其他配置的默认状态
global.GVA_DB.Model(&app.AIConfig{}).Where("id != ?", id).Update("is_default", false)
updates["is_default"] = true
}
return global.GVA_DB.Model(&config).Updates(updates).Error
}
// DeleteAIConfig 删除AI配置
func (s *AIConfigService) DeleteAIConfig(id uint) error {
result := global.GVA_DB.Delete(&app.AIConfig{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("配置不存在")
}
return nil
}
// GetModels 获取可用模型列表
func (s *AIConfigService) GetModels(req *request.GetModelsRequest) (*response.GetModelsResponse, error) {
client := &http.Client{Timeout: 20 * time.Second}
// 构建请求
modelsURL := req.BaseURL + "/models"
httpReq, err := http.NewRequest("GET", modelsURL, nil)
if err != nil {
return nil, err
}
// 设置认证头
if req.Provider == "openai" || req.Provider == "custom" {
httpReq.Header.Set("Authorization", "Bearer "+req.APIKey)
} else if req.Provider == "anthropic" {
httpReq.Header.Set("x-api-key", req.APIKey)
httpReq.Header.Set("anthropic-version", "2023-06-01")
}
// 发送请求
resp, err := client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API返回错误 %d: %s", resp.StatusCode, string(body))
}
// 解析响应
var result struct {
Data []struct {
ID string `json:"id"`
Object string `json:"object"`
OwnedBy string `json:"owned_by"`
} `json:"data"`
}
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
// 转换为模型信息
models := make([]response.ModelInfo, 0, len(result.Data))
for _, model := range result.Data {
models = append(models, response.ModelInfo{
ID: model.ID,
Name: model.ID,
OwnedBy: model.OwnedBy,
})
}
return &response.GetModelsResponse{Models: models}, nil
}
// GetModelsByID 通过配置ID获取模型列表使用数据库中的完整API Key
func (s *AIConfigService) GetModelsByID(id uint) (*response.GetModelsResponse, error) {
var config app.AIConfig
err := global.GVA_DB.First(&config, id).Error
if err != nil {
return nil, errors.New("配置不存在")
}
// 使用数据库中的完整 API Key
req := &request.GetModelsRequest{
Provider: config.Provider,
BaseURL: config.BaseURL,
APIKey: config.APIKey,
}
return s.GetModels(req)
}
// TestAIConfig 测试AI配置
func (s *AIConfigService) TestAIConfig(req *request.TestAIConfigRequest) (*response.TestAIConfigResponse, error) {
startTime := time.Now()
client := &http.Client{Timeout: 60 * time.Second}
// 构建测试请求
var requestBody map[string]interface{}
var endpoint string
if req.Provider == "openai" || req.Provider == "custom" {
endpoint = req.BaseURL + "/chat/completions"
model := req.Model
if model == "" {
model = "gpt-3.5-turbo"
}
requestBody = map[string]interface{}{
"model": model,
"messages": []map[string]string{
{"role": "user", "content": "Hello"},
},
"max_tokens": 10,
}
} else if req.Provider == "anthropic" {
endpoint = req.BaseURL + "/messages"
model := req.Model
if model == "" {
model = "claude-3-haiku-20240307"
}
requestBody = map[string]interface{}{
"model": model,
"messages": []map[string]string{
{"role": "user", "content": "Hello"},
},
"max_tokens": 10,
}
}
bodyBytes, _ := json.Marshal(requestBody)
httpReq, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(bodyBytes))
if err != nil {
return &response.TestAIConfigResponse{
Success: false,
Message: fmt.Sprintf("创建请求失败: %v", err),
}, nil
}
// 设置请求头
httpReq.Header.Set("Content-Type", "application/json")
if req.Provider == "openai" || req.Provider == "custom" {
httpReq.Header.Set("Authorization", "Bearer "+req.APIKey)
} else if req.Provider == "anthropic" {
httpReq.Header.Set("x-api-key", req.APIKey)
httpReq.Header.Set("anthropic-version", "2023-06-01")
}
// 发送请求
resp, err := client.Do(httpReq)
latency := time.Since(startTime).Milliseconds()
if err != nil {
return &response.TestAIConfigResponse{
Success: false,
Message: fmt.Sprintf("连接失败: %v", err),
Latency: latency,
}, nil
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return &response.TestAIConfigResponse{
Success: false,
Message: fmt.Sprintf("API返回错误 %d: %s", resp.StatusCode, string(body)),
Latency: latency,
}, nil
}
return &response.TestAIConfigResponse{
Success: true,
Message: "连接成功AI响应正常",
Latency: latency,
}, nil
}
// TestAIConfigByID 通过ID测试AI配置使用数据库中的完整API Key
func (s *AIConfigService) TestAIConfigByID(id uint) (*response.TestAIConfigResponse, error) {
var config app.AIConfig
err := global.GVA_DB.First(&config, id).Error
if err != nil {
return nil, errors.New("配置不存在")
}
// 使用数据库中的完整 API Key 进行测试
req := &request.TestAIConfigRequest{
Provider: config.Provider,
BaseURL: config.BaseURL,
APIKey: config.APIKey, // 使用完整的 API Key而不是脱敏后的
Model: config.DefaultModel,
}
return s.TestAIConfig(req)
}
// isMaskedAPIKey 检查是否是脱敏的 API Key
func isMaskedAPIKey(apiKey string) bool {
// 脱敏格式: xxxx****xxxx 或 ****
return len(apiKey) > 0 && (apiKey == "****" || (len(apiKey) > 8 && apiKey[4:len(apiKey)-4] == "****"))
}

254
server/service/app/auth.go Normal file
View File

@@ -0,0 +1,254 @@
package app
import (
"errors"
"time"
"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"
"git.echol.cn/loser/st/server/utils"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type AuthService struct{}
// Register 用户注册
func (s *AuthService) Register(req *request.RegisterRequest) error {
// 检查用户名是否已存在
var count int64
err := global.GVA_DB.Model(&app.AppUser{}).Where("username = ?", req.Username).Count(&count).Error
if err != nil {
return err
}
if count > 0 {
return errors.New("用户名已存在")
}
// 检查邮箱是否已存在
if req.Email != "" {
err = global.GVA_DB.Model(&app.AppUser{}).Where("email = ?", req.Email).Count(&count).Error
if err != nil {
return err
}
if count > 0 {
return errors.New("邮箱已被使用")
}
}
// 密码加密
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return errors.New("密码加密失败")
}
// 创建用户
user := app.AppUser{
UUID: uuid.New().String(),
Username: req.Username,
Password: string(hashedPassword),
NickName: req.NickName,
Email: req.Email,
Phone: req.Phone,
Status: "active",
Enable: true,
}
if user.NickName == "" {
user.NickName = req.Username
}
return global.GVA_DB.Create(&user).Error
}
// Login 用户登录
func (s *AuthService) Login(req *request.LoginRequest, ip string) (*response.LoginResponse, error) {
// 查询用户
var user app.AppUser
err := global.GVA_DB.Where("username = ?", req.Username).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户名或密码错误")
}
return nil, err
}
// 检查用户状态
if !user.Enable {
return nil, errors.New("账户已被禁用")
}
if user.Status != "active" {
return nil, errors.New("账户状态异常")
}
// 验证密码
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
if err != nil {
return nil, errors.New("用户名或密码错误")
}
// 生成 Token
token, expiresAt, err := utils.CreateAppToken(user.ID, user.Username)
if err != nil {
return nil, errors.New("Token 生成失败")
}
// 生成刷新 Token
refreshToken, refreshExpiresAt, err := utils.CreateAppRefreshToken(user.ID, user.Username)
if err != nil {
return nil, errors.New("刷新 Token 生成失败")
}
// 更新最后登录信息
now := time.Now()
global.GVA_DB.Model(&user).Updates(map[string]interface{}{
"last_login_at": now,
"last_login_ip": ip,
})
// 保存会话信息(可选)
session := app.AppUserSession{
UserID: user.ID,
SessionToken: token,
RefreshToken: refreshToken,
ExpiresAt: time.Unix(expiresAt, 0),
RefreshExpiresAt: func() *time.Time { t := time.Unix(refreshExpiresAt, 0); return &t }(),
IPAddress: ip,
}
global.GVA_DB.Create(&session)
return &response.LoginResponse{
User: response.ToAppUserResponse(&user),
Token: token,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
}, nil
}
// RefreshToken 刷新 Token
func (s *AuthService) RefreshToken(req *request.RefreshTokenRequest) (*response.LoginResponse, error) {
// 解析刷新 Token
claims, err := utils.ParseAppToken(req.RefreshToken)
if err != nil {
return nil, errors.New("刷新 Token 无效")
}
// 查询用户
var user app.AppUser
err = global.GVA_DB.Where("id = ?", claims.UserID).First(&user).Error
if err != nil {
return nil, errors.New("用户不存在")
}
// 检查用户状态
if !user.Enable {
return nil, errors.New("账户已被禁用")
}
// 生成新的 Token
token, expiresAt, err := utils.CreateAppToken(user.ID, user.Username)
if err != nil {
return nil, errors.New("Token 生成失败")
}
// 生成新的刷新 Token
refreshToken, _, err := utils.CreateAppRefreshToken(user.ID, user.Username)
if err != nil {
return nil, errors.New("刷新 Token 生成失败")
}
return &response.LoginResponse{
User: response.ToAppUserResponse(&user),
Token: token,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
}, nil
}
// Logout 用户登出
func (s *AuthService) Logout(userID uint, token string) error {
// 删除会话记录
return global.GVA_DB.Where("user_id = ? AND session_token = ?", userID, token).
Delete(&app.AppUserSession{}).Error
}
// GetUserInfo 获取用户信息
func (s *AuthService) GetUserInfo(userID uint) (*response.AppUserResponse, error) {
var user app.AppUser
err := global.GVA_DB.Where("id = ?", userID).First(&user).Error
if err != nil {
return nil, err
}
resp := response.ToAppUserResponse(&user)
return &resp, nil
}
// UpdateProfile 更新用户信息
func (s *AuthService) UpdateProfile(userID uint, req *request.UpdateProfileRequest) error {
updates := make(map[string]interface{})
if req.NickName != "" {
updates["nick_name"] = req.NickName
}
if req.Email != "" {
// 检查邮箱是否已被其他用户使用
var count int64
err := global.GVA_DB.Model(&app.AppUser{}).
Where("email = ? AND id != ?", req.Email, userID).
Count(&count).Error
if err != nil {
return err
}
if count > 0 {
return errors.New("邮箱已被使用")
}
updates["email"] = req.Email
}
if req.Phone != "" {
updates["phone"] = req.Phone
}
if req.Avatar != "" {
updates["avatar"] = req.Avatar
}
if req.Preferences != "" {
updates["preferences"] = req.Preferences
}
if req.AISettings != "" {
updates["ai_settings"] = req.AISettings
}
if len(updates) == 0 {
return nil
}
return global.GVA_DB.Model(&app.AppUser{}).Where("id = ?", userID).Updates(updates).Error
}
// ChangePassword 修改密码
func (s *AuthService) ChangePassword(userID uint, req *request.ChangePasswordRequest) error {
// 查询用户
var user app.AppUser
err := global.GVA_DB.Where("id = ?", userID).First(&user).Error
if err != nil {
return err
}
// 验证旧密码
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword))
if err != nil {
return errors.New("原密码错误")
}
// 加密新密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
return errors.New("密码加密失败")
}
// 更新密码
return global.GVA_DB.Model(&user).Update("password", string(hashedPassword)).Error
}

View File

@@ -0,0 +1,366 @@
package app
import (
"encoding/base64"
"encoding/json"
"errors"
"mime/multipart"
"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"
"git.echol.cn/loser/st/server/utils"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type CharacterService struct{}
// CreateCharacter 创建角色卡
func (s *CharacterService) CreateCharacter(userID uint, req *request.CreateCharacterRequest) (*response.CharacterResponse, error) {
// 序列化 JSON 字段
tagsJSON, _ := json.Marshal(req.Tags)
alternateGreetingsJSON, _ := json.Marshal(req.AlternateGreetings)
characterBookJSON, _ := json.Marshal(req.CharacterBook)
extensionsJSON, _ := json.Marshal(req.Extensions)
character := app.AICharacter{
UserID: userID,
Name: req.Name,
Avatar: req.Avatar,
Creator: req.Creator,
Version: req.Version,
Description: req.Description,
Personality: req.Personality,
Scenario: req.Scenario,
FirstMes: req.FirstMes,
MesExample: req.MesExample,
CreatorNotes: req.CreatorNotes,
SystemPrompt: req.SystemPrompt,
PostHistoryInstructions: req.PostHistoryInstructions,
Tags: datatypes.JSON(tagsJSON),
AlternateGreetings: datatypes.JSON(alternateGreetingsJSON),
CharacterBook: datatypes.JSON(characterBookJSON),
Extensions: datatypes.JSON(extensionsJSON),
IsPublic: req.IsPublic,
Spec: "chara_card_v2",
SpecVersion: "2.0",
}
err := global.GVA_DB.Create(&character).Error
if err != nil {
return nil, err
}
resp := response.ToCharacterResponse(&character)
return &resp, nil
}
// GetCharacterList 获取角色卡列表
func (s *CharacterService) GetCharacterList(userID uint, req *request.GetCharacterListRequest) (*response.CharacterListResponse, error) {
var characters []app.AICharacter
var total int64
db := global.GVA_DB.Model(&app.AICharacter{})
// 筛选条件
if req.IsPublic != nil {
if *req.IsPublic {
db = db.Where("is_public = ?", true)
} else {
db = db.Where("user_id = ?", userID)
}
} else {
db = db.Where("user_id = ? OR is_public = ?", userID, true)
}
// 关键词搜索
if req.Keyword != "" {
db = db.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 标签筛选
if req.Tag != "" {
db = db.Where("tags @> ?", datatypes.JSON(`["`+req.Tag+`"]`))
}
// 统计总数
err := db.Count(&total).Error
if err != nil {
return nil, err
}
// 分页查询
offset := (req.Page - 1) * req.PageSize
err = db.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&characters).Error
if err != nil {
return nil, err
}
// 转换响应
list := make([]response.CharacterResponse, len(characters))
for i, char := range characters {
list[i] = response.ToCharacterResponse(&char)
}
return &response.CharacterListResponse{
List: list,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
// GetCharacterByID 获取角色卡详情
func (s *CharacterService) GetCharacterByID(userID, characterID uint) (*response.CharacterResponse, error) {
var character app.AICharacter
err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", characterID, userID, true).
First(&character).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("角色卡不存在或无权访问")
}
return nil, err
}
resp := response.ToCharacterResponse(&character)
return &resp, nil
}
// UpdateCharacter 更新角色卡
func (s *CharacterService) UpdateCharacter(userID, characterID uint, req *request.UpdateCharacterRequest) error {
var character app.AICharacter
// 检查权限
err := global.GVA_DB.Where("id = ? AND user_id = ?", characterID, userID).First(&character).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("角色卡不存在或无权修改")
}
return err
}
// 更新字段
updates := map[string]interface{}{}
if req.Name != "" {
updates["name"] = req.Name
}
if req.Avatar != "" {
updates["avatar"] = req.Avatar
}
if req.Creator != "" {
updates["creator"] = req.Creator
}
if req.Version != "" {
updates["version"] = req.Version
}
if req.Description != "" {
updates["description"] = req.Description
}
if req.Personality != "" {
updates["personality"] = req.Personality
}
if req.Scenario != "" {
updates["scenario"] = req.Scenario
}
if req.FirstMes != "" {
updates["first_mes"] = req.FirstMes
}
if req.MesExample != "" {
updates["mes_example"] = req.MesExample
}
if req.CreatorNotes != "" {
updates["creator_notes"] = req.CreatorNotes
}
if req.SystemPrompt != "" {
updates["system_prompt"] = req.SystemPrompt
}
if req.PostHistoryInstructions != "" {
updates["post_history_instructions"] = req.PostHistoryInstructions
}
if req.Tags != nil {
tagsJSON, _ := json.Marshal(req.Tags)
updates["tags"] = datatypes.JSON(tagsJSON)
}
if req.AlternateGreetings != nil {
alternateGreetingsJSON, _ := json.Marshal(req.AlternateGreetings)
updates["alternate_greetings"] = datatypes.JSON(alternateGreetingsJSON)
}
if req.CharacterBook != nil {
characterBookJSON, _ := json.Marshal(req.CharacterBook)
updates["character_book"] = datatypes.JSON(characterBookJSON)
}
if req.Extensions != nil {
extensionsJSON, _ := json.Marshal(req.Extensions)
updates["extensions"] = datatypes.JSON(extensionsJSON)
}
updates["is_public"] = req.IsPublic
return global.GVA_DB.Model(&character).Updates(updates).Error
}
// DeleteCharacter 删除角色卡
func (s *CharacterService) DeleteCharacter(userID, characterID uint) error {
result := global.GVA_DB.Where("id = ? AND user_id = ?", characterID, userID).Delete(&app.AICharacter{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("角色卡不存在或无权删除")
}
return nil
}
// ImportCharacterFromPNG 从 PNG 文件导入角色卡
func (s *CharacterService) ImportCharacterFromPNG(userID uint, file *multipart.FileHeader) (*response.CharacterResponse, error) {
// 读取文件内容
src, err := file.Open()
if err != nil {
return nil, errors.New("打开文件失败")
}
defer src.Close()
// 读取文件数据
fileData := make([]byte, file.Size)
_, err = src.Read(fileData)
if err != nil {
return nil, errors.New("读取文件失败")
}
// 提取角色卡数据
card, err := utils.ExtractCharacterFromPNG(fileData)
if err != nil {
return nil, err
}
// 上传 PNG 图片到 OSS替代 Base64
var uploadService UploadService
avatarURL, err := uploadService.UploadImage(file)
if err != nil {
// 如果上传失败,回退到 Base64向后兼容
avatarURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(fileData)
}
// 转换为创建请求
req := &request.CreateCharacterRequest{
Name: card.Data.Name,
Avatar: avatarURL,
Creator: card.Data.Creator,
Version: card.Data.CharacterVersion,
Description: card.Data.Description,
Personality: card.Data.Personality,
Scenario: card.Data.Scenario,
FirstMes: card.Data.FirstMes,
MesExample: card.Data.MesExample,
CreatorNotes: card.Data.CreatorNotes,
SystemPrompt: card.Data.SystemPrompt,
PostHistoryInstructions: card.Data.PostHistoryInstructions,
Tags: card.Data.Tags,
AlternateGreetings: card.Data.AlternateGreetings,
CharacterBook: card.Data.CharacterBook,
Extensions: card.Data.Extensions,
IsPublic: false,
}
return s.CreateCharacter(userID, req)
}
// ImportCharacterFromJSON 从 JSON 文件导入角色卡
func (s *CharacterService) ImportCharacterFromJSON(userID uint, file *multipart.FileHeader) (*response.CharacterResponse, error) {
// 读取文件内容
src, err := file.Open()
if err != nil {
return nil, errors.New("打开文件失败")
}
defer src.Close()
// 读取文件数据
fileData := make([]byte, file.Size)
_, err = src.Read(fileData)
if err != nil {
return nil, errors.New("读取文件失败")
}
// 解析 JSON
card, err := utils.ParseCharacterCardJSON(fileData)
if err != nil {
return nil, err
}
// 转换为创建请求
req := &request.CreateCharacterRequest{
Name: card.Data.Name,
Creator: card.Data.Creator,
Version: card.Data.CharacterVersion,
Description: card.Data.Description,
Personality: card.Data.Personality,
Scenario: card.Data.Scenario,
FirstMes: card.Data.FirstMes,
MesExample: card.Data.MesExample,
CreatorNotes: card.Data.CreatorNotes,
SystemPrompt: card.Data.SystemPrompt,
PostHistoryInstructions: card.Data.PostHistoryInstructions,
Tags: card.Data.Tags,
AlternateGreetings: card.Data.AlternateGreetings,
CharacterBook: card.Data.CharacterBook,
Extensions: card.Data.Extensions,
IsPublic: false,
}
return s.CreateCharacter(userID, req)
}
// ExportCharacterToJSON 导出角色卡为 JSON
func (s *CharacterService) ExportCharacterToJSON(userID, characterID uint) (*utils.CharacterCardV2, error) {
var character app.AICharacter
err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", characterID, userID, true).
First(&character).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("角色卡不存在或无权访问")
}
return nil, err
}
// 解析 JSON 字段
var tags []string
var alternateGreetings []string
var characterBook map[string]interface{}
var extensions map[string]interface{}
json.Unmarshal(character.Tags, &tags)
json.Unmarshal(character.AlternateGreetings, &alternateGreetings)
json.Unmarshal(character.CharacterBook, &characterBook)
json.Unmarshal(character.Extensions, &extensions)
// 构建 V2 格式
card := &utils.CharacterCardV2{
Spec: character.Spec,
SpecVersion: character.SpecVersion,
Data: utils.CharacterCardV2Data{
Name: character.Name,
Description: character.Description,
Personality: character.Personality,
Scenario: character.Scenario,
FirstMes: character.FirstMes,
MesExample: character.MesExample,
CreatorNotes: character.CreatorNotes,
SystemPrompt: character.SystemPrompt,
PostHistoryInstructions: character.PostHistoryInstructions,
Tags: tags,
Creator: character.Creator,
CharacterVersion: character.Version,
AlternateGreetings: alternateGreetings,
CharacterBook: characterBook,
Extensions: extensions,
},
}
return card, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
package app
type AppServiceGroup struct {
AuthService
CharacterService
ConversationService
AIConfigService
PresetService
UploadService
}

View File

@@ -0,0 +1,353 @@
package app
import (
"encoding/json"
"errors"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/model/app"
"git.echol.cn/loser/st/server/model/app/request"
"go.uber.org/zap"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type PresetService struct{}
// CreatePreset 创建预设
func (s *PresetService) CreatePreset(userID uint, req *request.CreatePresetRequest) (*app.AIPreset, error) {
// 序列化 StopSequences
var stopSequencesJSON datatypes.JSON
if len(req.StopSequences) > 0 {
data, err := json.Marshal(req.StopSequences)
if err != nil {
global.GVA_LOG.Error("序列化 StopSequences 失败", zap.Error(err))
return nil, err
}
stopSequencesJSON = data
}
// 序列化 Extensions
var extensionsJSON datatypes.JSON
if len(req.Extensions) > 0 {
data, err := json.Marshal(req.Extensions)
if err != nil {
global.GVA_LOG.Error("序列化 Extensions 失败", zap.Error(err))
return nil, err
}
extensionsJSON = data
}
preset := &app.AIPreset{
UserID: userID,
Name: req.Name,
Description: req.Description,
IsPublic: req.IsPublic,
Temperature: req.Temperature,
TopP: req.TopP,
TopK: req.TopK,
FrequencyPenalty: req.FrequencyPenalty,
PresencePenalty: req.PresencePenalty,
MaxTokens: req.MaxTokens,
RepetitionPenalty: req.RepetitionPenalty,
MinP: req.MinP,
TopA: req.TopA,
SystemPrompt: req.SystemPrompt,
StopSequences: stopSequencesJSON,
Extensions: extensionsJSON,
}
if err := global.GVA_DB.Create(preset).Error; err != nil {
global.GVA_LOG.Error("创建预设失败", zap.Error(err))
return nil, err
}
return preset, nil
}
// GetPresetList 获取预设列表
func (s *PresetService) GetPresetList(userID uint, req *request.GetPresetListRequest) ([]app.AIPreset, int64, error) {
var presets []app.AIPreset
var total int64
db := global.GVA_DB.Model(&app.AIPreset{})
// 权限过滤:只能看到自己的预设或公开的预设
db = db.Where("user_id = ? OR is_public = ?", userID, true)
// 关键词搜索
if req.Keyword != "" {
db = db.Where("name LIKE ? OR description LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 公开/私有过滤
if req.IsPublic != nil {
db = db.Where("is_public = ?", *req.IsPublic)
}
// 获取总数
if err := db.Count(&total).Error; err != nil {
global.GVA_LOG.Error("获取预设总数失败", zap.Error(err))
return nil, 0, err
}
// 分页查询
offset := (req.Page - 1) * req.PageSize
if err := db.Order("is_default DESC, updated_at DESC").
Offset(offset).
Limit(req.PageSize).
Find(&presets).Error; err != nil {
global.GVA_LOG.Error("获取预设列表失败", zap.Error(err))
return nil, 0, err
}
return presets, total, nil
}
// GetPresetByID 根据ID获取预设
func (s *PresetService) GetPresetByID(userID uint, id uint) (*app.AIPreset, error) {
var preset app.AIPreset
// 权限检查:只能访问自己的预设或公开的预设
if err := global.GVA_DB.Where("id = ? AND (user_id = ? OR is_public = ?)", id, userID, true).
First(&preset).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("预设不存在或无权访问")
}
global.GVA_LOG.Error("获取预设失败", zap.Error(err))
return nil, err
}
return &preset, nil
}
// UpdatePreset 更新预设
func (s *PresetService) UpdatePreset(userID uint, id uint, req *request.UpdatePresetRequest) error {
var preset app.AIPreset
// 权限检查:只能更新自己的预设
if err := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).First(&preset).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("预设不存在或无权修改")
}
global.GVA_LOG.Error("查询预设失败", zap.Error(err))
return err
}
// 构建更新数据
updates := make(map[string]interface{})
if req.Name != "" {
updates["name"] = req.Name
}
if req.Description != "" {
updates["description"] = req.Description
}
if req.IsPublic != nil {
updates["is_public"] = *req.IsPublic
}
if req.Temperature != nil {
updates["temperature"] = *req.Temperature
}
if req.TopP != nil {
updates["top_p"] = *req.TopP
}
if req.TopK != nil {
updates["top_k"] = *req.TopK
}
if req.FrequencyPenalty != nil {
updates["frequency_penalty"] = *req.FrequencyPenalty
}
if req.PresencePenalty != nil {
updates["presence_penalty"] = *req.PresencePenalty
}
if req.MaxTokens != nil {
updates["max_tokens"] = *req.MaxTokens
}
if req.RepetitionPenalty != nil {
updates["repetition_penalty"] = *req.RepetitionPenalty
}
if req.MinP != nil {
updates["min_p"] = *req.MinP
}
if req.TopA != nil {
updates["top_a"] = *req.TopA
}
if req.SystemPrompt != nil {
updates["system_prompt"] = *req.SystemPrompt
}
// 更新 StopSequences
if req.StopSequences != nil {
data, err := json.Marshal(req.StopSequences)
if err != nil {
global.GVA_LOG.Error("序列化 StopSequences 失败", zap.Error(err))
return err
}
updates["stop_sequences"] = datatypes.JSON(data)
}
// 更新 Extensions
if req.Extensions != nil {
data, err := json.Marshal(req.Extensions)
if err != nil {
global.GVA_LOG.Error("序列化 Extensions 失败", zap.Error(err))
return err
}
updates["extensions"] = datatypes.JSON(data)
}
if err := global.GVA_DB.Model(&preset).Updates(updates).Error; err != nil {
global.GVA_LOG.Error("更新预设失败", zap.Error(err))
return err
}
return nil
}
// DeletePreset 删除预设
func (s *PresetService) DeletePreset(userID uint, id uint) error {
// 权限检查:只能删除自己的预设
result := global.GVA_DB.Where("id = ? AND user_id = ?", id, userID).Delete(&app.AIPreset{})
if result.Error != nil {
global.GVA_LOG.Error("删除预设失败", zap.Error(result.Error))
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("预设不存在或无权删除")
}
return nil
}
// SetDefaultPreset 设置默认预设
func (s *PresetService) SetDefaultPreset(userID uint, id uint) error {
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
// 检查预设是否存在且属于当前用户
var preset app.AIPreset
if err := tx.Where("id = ? AND user_id = ?", id, userID).First(&preset).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("预设不存在或无权访问")
}
return err
}
// 取消当前用户的所有默认预设
if err := tx.Model(&app.AIPreset{}).
Where("user_id = ? AND is_default = ?", userID, true).
Update("is_default", false).Error; err != nil {
return err
}
// 设置新的默认预设
if err := tx.Model(&preset).Update("is_default", true).Error; err != nil {
return err
}
return nil
})
}
// ImportPresetFromJSON 从JSON导入预设
func (s *PresetService) ImportPresetFromJSON(userID uint, jsonData []byte, filename string) (*app.AIPreset, error) {
// 尝试解析为 SillyTavern 格式
var stPreset struct {
Temperature float64 `json:"temperature"`
TopP float64 `json:"top_p"`
TopK int `json:"top_k"`
FrequencyPenalty float64 `json:"frequency_penalty"`
PresencePenalty float64 `json:"presence_penalty"`
MaxTokens int `json:"openai_max_tokens"`
RepetitionPenalty float64 `json:"repetition_penalty"`
MinP float64 `json:"min_p"`
TopA float64 `json:"top_a"`
StopSequences []string `json:"stop_sequences"`
Prompts []map[string]interface{} `json:"prompts"`
PromptOrder []map[string]interface{} `json:"prompt_order"`
}
if err := json.Unmarshal(jsonData, &stPreset); err != nil {
global.GVA_LOG.Error("解析预设JSON失败", zap.Error(err))
return nil, errors.New("无效的预设格式")
}
// 从文件名提取预设名称(去掉 .json 后缀)
name := filename
if len(name) > 5 && name[len(name)-5:] == ".json" {
name = name[:len(name)-5]
}
// 构建 extensions 对象,包含 prompts 和 prompt_order
extensions := map[string]interface{}{
"prompts": stPreset.Prompts,
"prompt_order": stPreset.PromptOrder,
}
// 转换为创建请求
req := &request.CreatePresetRequest{
Name: name,
Description: "从 SillyTavern 导入",
Temperature: stPreset.Temperature,
TopP: stPreset.TopP,
TopK: stPreset.TopK,
FrequencyPenalty: stPreset.FrequencyPenalty,
PresencePenalty: stPreset.PresencePenalty,
MaxTokens: stPreset.MaxTokens,
RepetitionPenalty: stPreset.RepetitionPenalty,
MinP: stPreset.MinP,
TopA: stPreset.TopA,
SystemPrompt: "",
StopSequences: stPreset.StopSequences,
Extensions: extensions,
}
return s.CreatePreset(userID, req)
}
// ExportPresetToJSON 导出预设为JSON
func (s *PresetService) ExportPresetToJSON(userID uint, id uint) ([]byte, error) {
preset, err := s.GetPresetByID(userID, id)
if err != nil {
return nil, err
}
// 解析 StopSequences
var stopSequences []string
if len(preset.StopSequences) > 0 {
json.Unmarshal(preset.StopSequences, &stopSequences)
}
// 解析 Extensions
var extensions map[string]interface{}
if len(preset.Extensions) > 0 {
json.Unmarshal(preset.Extensions, &extensions)
}
// 转换为 SillyTavern 格式
stPreset := map[string]interface{}{
"name": preset.Name,
"description": preset.Description,
"temperature": preset.Temperature,
"top_p": preset.TopP,
"top_k": preset.TopK,
"frequency_penalty": preset.FrequencyPenalty,
"presence_penalty": preset.PresencePenalty,
"max_tokens": preset.MaxTokens,
"repetition_penalty": preset.RepetitionPenalty,
"min_p": preset.MinP,
"top_a": preset.TopA,
"system_prompt": preset.SystemPrompt,
"stop_sequences": stopSequences,
"extensions": extensions,
}
return json.MarshalIndent(stPreset, "", " ")
}
// IncrementUseCount 增加使用次数
func (s *PresetService) IncrementUseCount(id uint) error {
return global.GVA_DB.Model(&app.AIPreset{}).
Where("id = ?", id).
Update("use_count", gorm.Expr("use_count + ?", 1)).Error
}

View File

@@ -0,0 +1,49 @@
package app
import (
"errors"
"mime/multipart"
"strings"
"git.echol.cn/loser/st/server/global"
"git.echol.cn/loser/st/server/utils/upload"
"go.uber.org/zap"
)
type UploadService struct{}
// UploadImage 上传图片到 OSS
// 返回图片的访问 URL
func (s *UploadService) UploadImage(header *multipart.FileHeader) (string, error) {
// 验证文件类型
if !isImageFile(header.Filename) {
return "", errors.New("只支持图片格式jpg, jpeg, png, gif, webp")
}
// 验证文件大小(限制 10MB
if header.Size > 10*1024*1024 {
return "", errors.New("图片大小不能超过 10MB")
}
// 使用 OSS 上传
oss := upload.NewOss()
filePath, _, uploadErr := oss.UploadFile(header)
if uploadErr != nil {
global.GVA_LOG.Error("图片上传失败", zap.Error(uploadErr))
return "", errors.New("图片上传失败")
}
return filePath, nil
}
// isImageFile 检查是否为图片文件
func isImageFile(filename string) bool {
ext := strings.ToLower(filename[strings.LastIndex(filename, ".")+1:])
imageExts := []string{"jpg", "jpeg", "png", "gif", "webp"}
for _, validExt := range imageExts {
if ext == validExt {
return true
}
}
return false
}