🎨 重构用户端前端为vue开发,完善基础类和角色相关接口
This commit is contained in:
84
server/utils/app_jwt.go
Normal file
84
server/utils/app_jwt.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.echol.cn/loser/st/server/global"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
UserTypeApp = "app" // 前台用户类型标识
|
||||
)
|
||||
|
||||
// AppJWTClaims 前台用户 JWT Claims
|
||||
type AppJWTClaims struct {
|
||||
UserID uint `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
UserType string `json:"userType"` // 用户类型标识
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// CreateAppToken 创建前台用户 Token(有效期 7 天)
|
||||
func CreateAppToken(userID uint, username string) (tokenString string, expiresAt int64, err error) {
|
||||
// Token 有效期为 7 天
|
||||
expiresTime := time.Now().Add(7 * 24 * time.Hour)
|
||||
expiresAt = expiresTime.Unix()
|
||||
|
||||
claims := AppJWTClaims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
UserType: UserTypeApp,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expiresTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: global.GVA_CONFIG.JWT.Issuer,
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err = token.SignedString([]byte(global.GVA_CONFIG.JWT.SigningKey))
|
||||
return
|
||||
}
|
||||
|
||||
// CreateAppRefreshToken 创建前台用户刷新 Token(有效期更长)
|
||||
func CreateAppRefreshToken(userID uint, username string) (tokenString string, expiresAt int64, err error) {
|
||||
// 刷新 Token 有效期为 7 天
|
||||
expiresTime := time.Now().Add(7 * 24 * time.Hour)
|
||||
expiresAt = expiresTime.Unix()
|
||||
|
||||
claims := AppJWTClaims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
UserType: UserTypeApp,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expiresTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: global.GVA_CONFIG.JWT.Issuer,
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err = token.SignedString([]byte(global.GVA_CONFIG.JWT.SigningKey))
|
||||
return
|
||||
}
|
||||
|
||||
// ParseAppToken 解析前台用户 Token
|
||||
func ParseAppToken(tokenString string) (*AppJWTClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &AppJWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(global.GVA_CONFIG.JWT.SigningKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*AppJWTClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
284
server/utils/character_card.go
Normal file
284
server/utils/character_card.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
)
|
||||
|
||||
// CharacterCardV2 SillyTavern 角色卡 V2 格式
|
||||
type CharacterCardV2 struct {
|
||||
Spec string `json:"spec"`
|
||||
SpecVersion string `json:"spec_version"`
|
||||
Data CharacterCardV2Data `json:"data"`
|
||||
}
|
||||
|
||||
type CharacterCardV2Data struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Personality string `json:"personality"`
|
||||
Scenario string `json:"scenario"`
|
||||
FirstMes string `json:"first_mes"`
|
||||
MesExample string `json:"mes_example"`
|
||||
CreatorNotes string `json:"creator_notes"`
|
||||
SystemPrompt string `json:"system_prompt"`
|
||||
PostHistoryInstructions string `json:"post_history_instructions"`
|
||||
Tags []string `json:"tags"`
|
||||
Creator string `json:"creator"`
|
||||
CharacterVersion string `json:"character_version"`
|
||||
AlternateGreetings []string `json:"alternate_greetings"`
|
||||
Extensions map[string]interface{} `json:"extensions"`
|
||||
}
|
||||
|
||||
// ExtractCharacterFromPNG 从 PNG 图片中提取角色卡数据
|
||||
func ExtractCharacterFromPNG(pngData []byte) (*CharacterCardV2, error) {
|
||||
reader := bytes.NewReader(pngData)
|
||||
|
||||
// 验证 PNG 格式(解码但不保存图片)
|
||||
_, err := png.Decode(reader)
|
||||
if err != nil {
|
||||
return nil, errors.New("无效的 PNG 文件")
|
||||
}
|
||||
|
||||
// 重新读取以获取 tEXt chunks
|
||||
reader.Seek(0, 0)
|
||||
|
||||
// 查找 tEXt chunk 中的 "chara" 字段
|
||||
charaJSON, err := extractTextChunk(reader, "chara")
|
||||
if err != nil {
|
||||
return nil, errors.New("PNG 中没有找到角色卡数据")
|
||||
}
|
||||
|
||||
// 尝试 Base64 解码
|
||||
decodedJSON, err := base64.StdEncoding.DecodeString(charaJSON)
|
||||
if err != nil {
|
||||
// 如果不是 Base64,直接使用原始 JSON
|
||||
decodedJSON = []byte(charaJSON)
|
||||
}
|
||||
|
||||
// 解析 JSON
|
||||
var card CharacterCardV2
|
||||
err = json.Unmarshal(decodedJSON, &card)
|
||||
if err != nil {
|
||||
return nil, errors.New("解析角色卡数据失败: " + err.Error())
|
||||
}
|
||||
|
||||
return &card, nil
|
||||
}
|
||||
|
||||
// extractTextChunk 从 PNG 中提取指定 key 的 tEXt chunk
|
||||
func extractTextChunk(r io.Reader, key string) (string, error) {
|
||||
// 跳过 PNG signature (8 bytes)
|
||||
signature := make([]byte, 8)
|
||||
if _, err := io.ReadFull(r, signature); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 验证 PNG signature
|
||||
expectedSig := []byte{137, 80, 78, 71, 13, 10, 26, 10}
|
||||
if !bytes.Equal(signature, expectedSig) {
|
||||
return "", errors.New("invalid PNG signature")
|
||||
}
|
||||
|
||||
// 读取所有 chunks
|
||||
for {
|
||||
// 读取 chunk length (4 bytes)
|
||||
lengthBytes := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, lengthBytes); err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
length := uint32(lengthBytes[0])<<24 | uint32(lengthBytes[1])<<16 |
|
||||
uint32(lengthBytes[2])<<8 | uint32(lengthBytes[3])
|
||||
|
||||
// 读取 chunk type (4 bytes)
|
||||
chunkType := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, chunkType); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 读取 chunk data
|
||||
data := make([]byte, length)
|
||||
if _, err := io.ReadFull(r, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 读取 CRC (4 bytes)
|
||||
crc := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, crc); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 检查是否是 tEXt chunk
|
||||
if string(chunkType) == "tEXt" {
|
||||
// tEXt chunk 格式: keyword\0text
|
||||
nullIndex := bytes.IndexByte(data, 0)
|
||||
if nullIndex == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
keyword := string(data[:nullIndex])
|
||||
text := string(data[nullIndex+1:])
|
||||
|
||||
if keyword == key {
|
||||
return text, nil
|
||||
}
|
||||
}
|
||||
|
||||
// IEND chunk 表示结束
|
||||
if string(chunkType) == "IEND" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("text chunk not found")
|
||||
}
|
||||
|
||||
// EmbedCharacterToPNG 将角色卡数据嵌入到 PNG 图片中
|
||||
func EmbedCharacterToPNG(img image.Image, card *CharacterCardV2) ([]byte, error) {
|
||||
// 序列化角色卡数据
|
||||
cardJSON, err := json.Marshal(card)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Base64 编码
|
||||
encodedJSON := base64.StdEncoding.EncodeToString(cardJSON)
|
||||
|
||||
// 创建一个 buffer 来写入 PNG
|
||||
var buf bytes.Buffer
|
||||
|
||||
// 写入 PNG signature
|
||||
buf.Write([]byte{137, 80, 78, 71, 13, 10, 26, 10})
|
||||
|
||||
// 编码原始图片到临时 buffer
|
||||
var imgBuf bytes.Buffer
|
||||
if err := png.Encode(&imgBuf, img); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 跳过原始 PNG 的 signature
|
||||
imgData := imgBuf.Bytes()[8:]
|
||||
|
||||
// 将原始图片的 chunks 复制到输出,在 IEND 之前插入 tEXt chunk
|
||||
r := bytes.NewReader(imgData)
|
||||
|
||||
for {
|
||||
// 读取 chunk length
|
||||
lengthBytes := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, lengthBytes); err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
length := uint32(lengthBytes[0])<<24 | uint32(lengthBytes[1])<<16 |
|
||||
uint32(lengthBytes[2])<<8 | uint32(lengthBytes[3])
|
||||
|
||||
// 读取 chunk type
|
||||
chunkType := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, chunkType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 读取 chunk data
|
||||
data := make([]byte, length)
|
||||
if _, err := io.ReadFull(r, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 读取 CRC
|
||||
crc := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, crc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果是 IEND chunk,先写入 tEXt chunk
|
||||
if string(chunkType) == "IEND" {
|
||||
// 写入 tEXt chunk
|
||||
writeTextChunk(&buf, "chara", encodedJSON)
|
||||
}
|
||||
|
||||
// 写入原始 chunk
|
||||
buf.Write(lengthBytes)
|
||||
buf.Write(chunkType)
|
||||
buf.Write(data)
|
||||
buf.Write(crc)
|
||||
|
||||
if string(chunkType) == "IEND" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// writeTextChunk 写入 tEXt chunk
|
||||
func writeTextChunk(w io.Writer, keyword, text string) error {
|
||||
data := append([]byte(keyword), 0)
|
||||
data = append(data, []byte(text)...)
|
||||
|
||||
// 写入 length
|
||||
length := uint32(len(data))
|
||||
lengthBytes := []byte{
|
||||
byte(length >> 24),
|
||||
byte(length >> 16),
|
||||
byte(length >> 8),
|
||||
byte(length),
|
||||
}
|
||||
w.Write(lengthBytes)
|
||||
|
||||
// 写入 type
|
||||
w.Write([]byte("tEXt"))
|
||||
|
||||
// 写入 data
|
||||
w.Write(data)
|
||||
|
||||
// 计算并写入 CRC
|
||||
crcData := append([]byte("tEXt"), data...)
|
||||
crc := calculateCRC(crcData)
|
||||
crcBytes := []byte{
|
||||
byte(crc >> 24),
|
||||
byte(crc >> 16),
|
||||
byte(crc >> 8),
|
||||
byte(crc),
|
||||
}
|
||||
w.Write(crcBytes)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateCRC 计算 CRC32
|
||||
func calculateCRC(data []byte) uint32 {
|
||||
crc := uint32(0xFFFFFFFF)
|
||||
|
||||
for _, b := range data {
|
||||
crc ^= uint32(b)
|
||||
for i := 0; i < 8; i++ {
|
||||
if crc&1 != 0 {
|
||||
crc = (crc >> 1) ^ 0xEDB88320
|
||||
} else {
|
||||
crc >>= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return crc ^ 0xFFFFFFFF
|
||||
}
|
||||
|
||||
// ParseCharacterCardJSON 解析 JSON 格式的角色卡
|
||||
func ParseCharacterCardJSON(jsonData []byte) (*CharacterCardV2, error) {
|
||||
var card CharacterCardV2
|
||||
err := json.Unmarshal(jsonData, &card)
|
||||
if err != nil {
|
||||
return nil, errors.New("解析角色卡 JSON 失败: " + err.Error())
|
||||
}
|
||||
|
||||
return &card, nil
|
||||
}
|
||||
Reference in New Issue
Block a user