mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 20:41:38 +00:00
Compare commits
70 Commits
v0.9.9-pat
...
v0.9.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f419a3c71 | ||
|
|
c4ea095ae0 | ||
|
|
1ded19795a | ||
|
|
158b46eb4b | ||
|
|
2581bea93e | ||
|
|
c3ed6a689e | ||
|
|
f60896a838 | ||
|
|
36c603f3b2 | ||
|
|
1c582cde31 | ||
|
|
94a6f3eb57 | ||
|
|
3058fae145 | ||
|
|
687e455051 | ||
|
|
39a4c9ac02 | ||
|
|
47bfea1eae | ||
|
|
d7b6d0cd34 | ||
|
|
2b70095b47 | ||
|
|
45ebcd4f11 | ||
|
|
3dbe0c2067 | ||
|
|
0654718b34 | ||
|
|
6791eb72ba | ||
|
|
cb3537f529 | ||
|
|
471fd3a3b2 | ||
|
|
810641a264 | ||
|
|
b7896585fd | ||
|
|
179697ba61 | ||
|
|
032f159509 | ||
|
|
95a2d02df9 | ||
|
|
3ac9ff6028 | ||
|
|
fcf0f952b1 | ||
|
|
b99099fcbe | ||
|
|
bf66bbe5fa | ||
|
|
e80b442dd6 | ||
|
|
431b3a84f6 | ||
|
|
098e6e7f2b | ||
|
|
afcbff6644 | ||
|
|
ce1fde8500 | ||
|
|
4661399639 | ||
|
|
ac3baacec7 | ||
|
|
78d8d458ca | ||
|
|
e20a287c4b | ||
|
|
c7ab0f4f3d | ||
|
|
0d1057830b | ||
|
|
dd1cac3f2e | ||
|
|
cdbc7a9510 | ||
|
|
c693bfee5e | ||
|
|
7156bf2382 | ||
|
|
c216527f23 | ||
|
|
b1de0f49df | ||
|
|
525ca09f2c | ||
|
|
92fc973bc3 | ||
|
|
22ff8e2cbe | ||
|
|
1ec664a348 | ||
|
|
6a24c37c0e | ||
|
|
8965fc49c9 | ||
|
|
735386c0b9 | ||
|
|
58c4da0ddf | ||
|
|
fe68488b1c | ||
|
|
25af6e6f77 | ||
|
|
e2d3b46a3a | ||
|
|
dd775167ab | ||
|
|
fade73d970 | ||
|
|
6ccb404c94 | ||
|
|
95cb7fc862 | ||
|
|
01925858ec | ||
|
|
0c01fd406d | ||
|
|
a97dbdf95c | ||
|
|
7e46c43f6f | ||
|
|
a7d6a8b0d0 | ||
|
|
dc6fbffa96 | ||
|
|
99b9a34e19 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.idea
|
||||
.vscode
|
||||
.zed
|
||||
upload
|
||||
*.exe
|
||||
*.db
|
||||
@@ -10,10 +11,11 @@ web/dist
|
||||
.env
|
||||
one-api
|
||||
new-api
|
||||
/__debug_bin*
|
||||
.DS_Store
|
||||
tiktoken_cache
|
||||
.eslintcache
|
||||
.gocache
|
||||
|
||||
electron/node_modules
|
||||
electron/dist
|
||||
electron/dist
|
||||
|
||||
@@ -28,7 +28,7 @@ RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$
|
||||
FROM alpine
|
||||
|
||||
RUN apk upgrade --no-cache \
|
||||
&& apk add --no-cache ca-certificates tzdata ffmpeg \
|
||||
&& apk add --no-cache ca-certificates tzdata \
|
||||
&& update-ca-certificates
|
||||
|
||||
COPY --from=builder2 /build/new-api /
|
||||
|
||||
@@ -69,6 +69,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
|
||||
apiType = constant.APITypeMoonshot
|
||||
case constant.ChannelTypeSubmodel:
|
||||
apiType = constant.APITypeSubmodel
|
||||
case constant.ChannelTypeMiniMax:
|
||||
apiType = constant.APITypeMiniMax
|
||||
}
|
||||
if apiType == -1 {
|
||||
return constant.APITypeOpenAI, false
|
||||
|
||||
295
common/audio.go
Normal file
295
common/audio.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/abema/go-mp4"
|
||||
"github.com/go-audio/aiff"
|
||||
"github.com/go-audio/wav"
|
||||
"github.com/jfreymuth/oggvorbis"
|
||||
"github.com/mewkiz/flac"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tcolgate/mp3"
|
||||
"github.com/yapingcat/gomedia/go-codec"
|
||||
)
|
||||
|
||||
// GetAudioDuration 使用纯 Go 库获取音频文件的时长(秒)。
|
||||
// 它不再依赖外部的 ffmpeg 或 ffprobe 程序。
|
||||
func GetAudioDuration(ctx context.Context, f io.ReadSeeker, ext string) (duration float64, err error) {
|
||||
SysLog(fmt.Sprintf("GetAudioDuration: ext=%s", ext))
|
||||
// 根据文件扩展名选择解析器
|
||||
switch ext {
|
||||
case ".mp3":
|
||||
duration, err = getMP3Duration(f)
|
||||
case ".wav":
|
||||
duration, err = getWAVDuration(f)
|
||||
case ".flac":
|
||||
duration, err = getFLACDuration(f)
|
||||
case ".m4a", ".mp4":
|
||||
duration, err = getM4ADuration(f)
|
||||
case ".ogg", ".oga":
|
||||
duration, err = getOGGDuration(f)
|
||||
case ".opus":
|
||||
duration, err = getOpusDuration(f)
|
||||
case ".aiff", ".aif", ".aifc":
|
||||
duration, err = getAIFFDuration(f)
|
||||
case ".webm":
|
||||
duration, err = getWebMDuration(f)
|
||||
case ".aac":
|
||||
duration, err = getAACDuration(f)
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported audio format: %s", ext)
|
||||
}
|
||||
SysLog(fmt.Sprintf("GetAudioDuration: duration=%f", duration))
|
||||
return duration, err
|
||||
}
|
||||
|
||||
// getMP3Duration 解析 MP3 文件以获取时长。
|
||||
// 注意:对于 VBR (Variable Bitrate) MP3,这个估算可能不完全精确,但通常足够好。
|
||||
// FFmpeg 在这种情况下会扫描整个文件来获得精确值,但这里的库提供了快速估算。
|
||||
func getMP3Duration(r io.Reader) (float64, error) {
|
||||
d := mp3.NewDecoder(r)
|
||||
var f mp3.Frame
|
||||
skipped := 0
|
||||
duration := 0.0
|
||||
|
||||
for {
|
||||
if err := d.Decode(&f, &skipped); err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return 0, errors.Wrap(err, "failed to decode mp3 frame")
|
||||
}
|
||||
duration += f.Duration().Seconds()
|
||||
}
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
// getWAVDuration 解析 WAV 文件头以获取时长。
|
||||
func getWAVDuration(r io.ReadSeeker) (float64, error) {
|
||||
dec := wav.NewDecoder(r)
|
||||
if !dec.IsValidFile() {
|
||||
return 0, errors.New("invalid wav file")
|
||||
}
|
||||
d, err := dec.Duration()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to get wav duration")
|
||||
}
|
||||
return d.Seconds(), nil
|
||||
}
|
||||
|
||||
// getFLACDuration 解析 FLAC 文件的 STREAMINFO 块。
|
||||
func getFLACDuration(r io.Reader) (float64, error) {
|
||||
stream, err := flac.Parse(r)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to parse flac stream")
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
// 时长 = 总采样数 / 采样率
|
||||
duration := float64(stream.Info.NSamples) / float64(stream.Info.SampleRate)
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
// getM4ADuration 解析 M4A/MP4 文件的 'mvhd' box。
|
||||
func getM4ADuration(r io.ReadSeeker) (float64, error) {
|
||||
// go-mp4 库需要 ReadSeeker 接口
|
||||
info, err := mp4.Probe(r)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to probe m4a/mp4 file")
|
||||
}
|
||||
// 时长 = Duration / Timescale
|
||||
return float64(info.Duration) / float64(info.Timescale), nil
|
||||
}
|
||||
|
||||
// getOGGDuration 解析 OGG/Vorbis 文件以获取时长。
|
||||
func getOGGDuration(r io.ReadSeeker) (float64, error) {
|
||||
// 重置 reader 到开头
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return 0, errors.Wrap(err, "failed to seek ogg file")
|
||||
}
|
||||
|
||||
reader, err := oggvorbis.NewReader(r)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to create ogg vorbis reader")
|
||||
}
|
||||
|
||||
// 计算时长 = 总采样数 / 采样率
|
||||
// 需要读取整个文件来获取总采样数
|
||||
channels := reader.Channels()
|
||||
sampleRate := reader.SampleRate()
|
||||
|
||||
// 估算方法:读取到文件结尾
|
||||
var totalSamples int64
|
||||
buf := make([]float32, 4096*channels)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to read ogg samples")
|
||||
}
|
||||
totalSamples += int64(n / channels)
|
||||
}
|
||||
|
||||
duration := float64(totalSamples) / float64(sampleRate)
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
// getOpusDuration 解析 Opus 文件(在 OGG 容器中)以获取时长。
|
||||
func getOpusDuration(r io.ReadSeeker) (float64, error) {
|
||||
// Opus 通常封装在 OGG 容器中
|
||||
// 我们需要解析 OGG 页面来获取时长信息
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return 0, errors.Wrap(err, "failed to seek opus file")
|
||||
}
|
||||
|
||||
// 读取 OGG 页面头部
|
||||
var totalGranulePos int64
|
||||
buf := make([]byte, 27) // OGG 页面头部最小大小
|
||||
|
||||
for {
|
||||
n, err := r.Read(buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to read opus/ogg page")
|
||||
}
|
||||
if n < 27 {
|
||||
break
|
||||
}
|
||||
|
||||
// 检查 OGG 页面标识 "OggS"
|
||||
if string(buf[0:4]) != "OggS" {
|
||||
// 跳过一些字节继续寻找
|
||||
if _, err := r.Seek(-26, io.SeekCurrent); err != nil {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取 granule position (字节 6-13, 小端序)
|
||||
granulePos := int64(binary.LittleEndian.Uint64(buf[6:14]))
|
||||
if granulePos > totalGranulePos {
|
||||
totalGranulePos = granulePos
|
||||
}
|
||||
|
||||
// 读取段表大小
|
||||
numSegments := int(buf[26])
|
||||
segmentTable := make([]byte, numSegments)
|
||||
if _, err := io.ReadFull(r, segmentTable); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// 计算页面数据大小并跳过
|
||||
var pageSize int
|
||||
for _, segSize := range segmentTable {
|
||||
pageSize += int(segSize)
|
||||
}
|
||||
if _, err := r.Seek(int64(pageSize), io.SeekCurrent); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Opus 的采样率固定为 48000 Hz
|
||||
duration := float64(totalGranulePos) / 48000.0
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
// getAIFFDuration 解析 AIFF 文件头以获取时长。
|
||||
func getAIFFDuration(r io.ReadSeeker) (float64, error) {
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return 0, errors.Wrap(err, "failed to seek aiff file")
|
||||
}
|
||||
|
||||
dec := aiff.NewDecoder(r)
|
||||
if !dec.IsValidFile() {
|
||||
return 0, errors.New("invalid aiff file")
|
||||
}
|
||||
|
||||
d, err := dec.Duration()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to get aiff duration")
|
||||
}
|
||||
|
||||
return d.Seconds(), nil
|
||||
}
|
||||
|
||||
// getWebMDuration 解析 WebM 文件以获取时长。
|
||||
// WebM 使用 Matroska 容器格式
|
||||
func getWebMDuration(r io.ReadSeeker) (float64, error) {
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return 0, errors.Wrap(err, "failed to seek webm file")
|
||||
}
|
||||
|
||||
// WebM/Matroska 文件的解析比较复杂
|
||||
// 这里提供一个简化的实现,读取 EBML 头部
|
||||
// 对于完整的 WebM 解析,可能需要使用专门的库
|
||||
|
||||
// 简单实现:查找 Duration 元素
|
||||
// WebM Duration 的 Element ID 是 0x4489
|
||||
// 这是一个简化版本,可能不适用于所有 WebM 文件
|
||||
buf := make([]byte, 8192)
|
||||
n, err := r.Read(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
return 0, errors.Wrap(err, "failed to read webm file")
|
||||
}
|
||||
|
||||
// 尝试查找 Duration 元素(这是一个简化的方法)
|
||||
// 实际的 WebM 解析需要完整的 EBML 解析器
|
||||
// 这里返回错误,建议使用专门的库
|
||||
if n > 0 {
|
||||
// 检查 EBML 标识
|
||||
if len(buf) >= 4 && binary.BigEndian.Uint32(buf[0:4]) == 0x1A45DFA3 {
|
||||
// 这是一个有效的 EBML 文件
|
||||
// 但完整解析需要更复杂的逻辑
|
||||
return 0, errors.New("webm duration parsing requires full EBML parser (consider using ffprobe for webm files)")
|
||||
}
|
||||
}
|
||||
|
||||
return 0, errors.New("failed to parse webm file")
|
||||
}
|
||||
|
||||
// getAACDuration 解析 AAC (ADTS格式) 文件以获取时长。
|
||||
// 使用 gomedia 库来解析 AAC ADTS 帧
|
||||
func getAACDuration(r io.ReadSeeker) (float64, error) {
|
||||
if _, err := r.Seek(0, io.SeekStart); err != nil {
|
||||
return 0, errors.Wrap(err, "failed to seek aac file")
|
||||
}
|
||||
|
||||
// 读取整个文件内容
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to read aac file")
|
||||
}
|
||||
|
||||
var totalFrames int64
|
||||
var sampleRate int
|
||||
|
||||
// 使用 gomedia 的 SplitAACFrame 函数来分割 AAC 帧
|
||||
codec.SplitAACFrame(data, func(aac []byte) {
|
||||
// 解析 ADTS 头部以获取采样率信息
|
||||
if len(aac) >= 7 {
|
||||
// 使用 ConvertADTSToASC 来获取音频配置信息
|
||||
asc, err := codec.ConvertADTSToASC(aac)
|
||||
if err == nil && sampleRate == 0 {
|
||||
sampleRate = codec.AACSampleIdxToSample(int(asc.Sample_freq_index))
|
||||
}
|
||||
totalFrames++
|
||||
}
|
||||
})
|
||||
|
||||
if sampleRate == 0 || totalFrames == 0 {
|
||||
return 0, errors.New("no valid aac frames found")
|
||||
}
|
||||
|
||||
// 每个 AAC ADTS 帧包含 1024 个采样
|
||||
totalSamples := totalFrames * 1024
|
||||
duration := float64(totalSamples) / float64(sampleRate)
|
||||
return duration, nil
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -40,6 +42,10 @@ func UnmarshalBodyReusable(c *gin.Context, v any) error {
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
err = Unmarshal(requestBody, &v)
|
||||
} else if strings.Contains(contentType, gin.MIMEPOSTForm) {
|
||||
err = parseFormData(requestBody, &v)
|
||||
} else if strings.Contains(contentType, gin.MIMEMultipartPOSTForm) {
|
||||
err = parseMultipartFormData(c, requestBody, &v)
|
||||
} else {
|
||||
// skip for now
|
||||
// TODO: someday non json request have variant model, we will need to implementation this
|
||||
@@ -138,3 +144,57 @@ func ParseMultipartFormReusable(c *gin.Context) (*multipart.Form, error) {
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
||||
return form, nil
|
||||
}
|
||||
|
||||
func parseFormData(data []byte, v any) error {
|
||||
values, err := url.ParseQuery(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formMap := make(map[string]any)
|
||||
for key, vals := range values {
|
||||
if len(vals) == 1 {
|
||||
formMap[key] = vals[0]
|
||||
} else {
|
||||
formMap[key] = vals
|
||||
}
|
||||
}
|
||||
jsonData, err := json.Marshal(formMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Unmarshal(jsonData, v)
|
||||
}
|
||||
|
||||
func parseMultipartFormData(c *gin.Context, data []byte, v any) error {
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
boundary := ""
|
||||
if idx := strings.Index(contentType, "boundary="); idx != -1 {
|
||||
boundary = contentType[idx+9:]
|
||||
}
|
||||
|
||||
if boundary == "" {
|
||||
return Unmarshal(data, v) // Fallback to JSON
|
||||
}
|
||||
|
||||
reader := multipart.NewReader(bytes.NewReader(data), boundary)
|
||||
form, err := reader.ReadForm(32 << 20) // 32 MB max memory
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer form.RemoveAll()
|
||||
formMap := make(map[string]any)
|
||||
for key, vals := range form.Value {
|
||||
if len(vals) == 1 {
|
||||
formMap[key] = vals[0]
|
||||
} else {
|
||||
formMap[key] = vals
|
||||
}
|
||||
}
|
||||
jsonData, err := Marshal(formMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Unmarshal(jsonData, v)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -329,43 +327,6 @@ func SaveTmpFile(filename string, data io.Reader) (string, error) {
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
// GetAudioDuration returns the duration of an audio file in seconds.
|
||||
func GetAudioDuration(ctx context.Context, filename string, ext string) (float64, error) {
|
||||
// ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {{input}}
|
||||
c := exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filename)
|
||||
output, err := c.Output()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to get audio duration")
|
||||
}
|
||||
durationStr := string(bytes.TrimSpace(output))
|
||||
if durationStr == "N/A" {
|
||||
// Create a temporary output file name
|
||||
tmpFp, err := os.CreateTemp("", "audio-*"+ext)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to create temporary file")
|
||||
}
|
||||
tmpName := tmpFp.Name()
|
||||
// Close immediately so ffmpeg can open the file on Windows.
|
||||
_ = tmpFp.Close()
|
||||
defer os.Remove(tmpName)
|
||||
|
||||
// ffmpeg -y -i filename -vcodec copy -acodec copy <tmpName>
|
||||
ffmpegCmd := exec.CommandContext(ctx, "ffmpeg", "-y", "-i", filename, "-vcodec", "copy", "-acodec", "copy", tmpName)
|
||||
if err := ffmpegCmd.Run(); err != nil {
|
||||
return 0, errors.Wrap(err, "failed to run ffmpeg")
|
||||
}
|
||||
|
||||
// Recalculate the duration of the new file
|
||||
c = exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", tmpName)
|
||||
output, err := c.Output()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "failed to get audio duration after ffmpeg")
|
||||
}
|
||||
durationStr = string(bytes.TrimSpace(output))
|
||||
}
|
||||
return strconv.ParseFloat(durationStr, 64)
|
||||
}
|
||||
|
||||
// BuildURL concatenates base and endpoint, returns the complete url string
|
||||
func BuildURL(base string, endpoint string) string {
|
||||
u, err := url.Parse(base)
|
||||
|
||||
@@ -33,5 +33,6 @@ const (
|
||||
APITypeJimeng
|
||||
APITypeMoonshot
|
||||
APITypeSubmodel
|
||||
APITypeMiniMax
|
||||
APITypeDummy // this one is only for count, do not add any channel after this
|
||||
)
|
||||
|
||||
@@ -620,13 +620,13 @@ func AutomaticallyTestChannels() {
|
||||
autoTestChannelsOnce.Do(func() {
|
||||
for {
|
||||
if !operation_setting.GetMonitorSetting().AutoTestChannelEnabled {
|
||||
time.Sleep(10 * time.Minute)
|
||||
time.Sleep(1 * time.Minute)
|
||||
continue
|
||||
}
|
||||
for {
|
||||
frequency := operation_setting.GetMonitorSetting().AutoTestChannelMinutes
|
||||
time.Sleep(time.Duration(frequency) * time.Minute)
|
||||
common.SysLog(fmt.Sprintf("automatically test channels with interval %d minutes", frequency))
|
||||
time.Sleep(time.Duration(int(math.Round(frequency))) * time.Minute)
|
||||
common.SysLog(fmt.Sprintf("automatically test channels with interval %f minutes", frequency))
|
||||
common.SysLog("automatically testing all channels")
|
||||
_ = testAllChannels(false)
|
||||
common.SysLog("automatically channel test finished")
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
|
||||
@@ -27,9 +28,9 @@ func GetUserGroups(c *gin.Context) {
|
||||
userGroup := ""
|
||||
userId := c.GetInt("id")
|
||||
userGroup, _ = model.GetUserGroup(userId, false)
|
||||
userUsableGroups := service.GetUserUsableGroups(userGroup)
|
||||
for groupName, ratio := range ratio_setting.GetGroupRatioCopy() {
|
||||
// UserUsableGroups contains the groups that the user can use
|
||||
userUsableGroups := setting.GetUserUsableGroups(userGroup)
|
||||
if desc, ok := userUsableGroups[groupName]; ok {
|
||||
usableGroups[groupName] = map[string]interface{}{
|
||||
"ratio": ratio,
|
||||
@@ -37,7 +38,7 @@ func GetUserGroups(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if setting.GroupInUserUsableGroups("auto") {
|
||||
if _, ok := userUsableGroups["auto"]; ok {
|
||||
usableGroups["auto"] = map[string]interface{}{
|
||||
"ratio": "自动",
|
||||
"desc": setting.GetUsableGroupDescription("auto"),
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/QuantumNous/new-api/relay/channel/minimax"
|
||||
"github.com/QuantumNous/new-api/relay/channel/moonshot"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
@@ -149,7 +149,7 @@ func ListModels(c *gin.Context, modelType int) {
|
||||
}
|
||||
var models []string
|
||||
if tokenGroup == "auto" {
|
||||
for _, autoGroup := range setting.AutoGroups {
|
||||
for _, autoGroup := range service.GetUserAutoGroup(userGroup) {
|
||||
groupModels := model.GetGroupEnabledModels(autoGroup)
|
||||
for _, g := range groupModels {
|
||||
if !common.StringsContains(models, g) {
|
||||
|
||||
@@ -2,7 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -30,7 +30,7 @@ func GetPricing(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
usableGroup = setting.GetUserUsableGroups(group)
|
||||
usableGroup = service.GetUserUsableGroups(group)
|
||||
// check groupRatio contains usableGroup
|
||||
for group := range ratio_setting.GetGroupRatioCopy() {
|
||||
if _, ok := usableGroup[group]; !ok {
|
||||
@@ -45,7 +45,7 @@ func GetPricing(c *gin.Context) {
|
||||
"group_ratio": groupRatio,
|
||||
"usable_group": usableGroup,
|
||||
"supported_endpoint": model.GetSupportedEndpointMap(),
|
||||
"auto_groups": setting.AutoGroups,
|
||||
"auto_groups": service.GetUserAutoGroup(group),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m
|
||||
AutoBan: &autoBanInt,
|
||||
}, nil
|
||||
}
|
||||
channel, selectGroup, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, retryCount)
|
||||
channel, selectGroup, err := service.CacheGetRandomSatisfiedChannel(c, group, originalModel, retryCount)
|
||||
if err != nil {
|
||||
return nil, types.NewError(fmt.Errorf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, originalModel, err.Error()), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry())
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ func GetTopUpInfo(c *gin.Context) {
|
||||
data := gin.H{
|
||||
"enable_online_topup": operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != "",
|
||||
"enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
|
||||
"enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]",
|
||||
"creem_products": setting.CreemProducts,
|
||||
"pay_methods": payMethods,
|
||||
"min_topup": operation_setting.MinTopUp,
|
||||
"stripe_min_topup": setting.StripeMinTopUp,
|
||||
|
||||
461
controller/topup_creem.go
Normal file
461
controller/topup_creem.go
Normal file
@@ -0,0 +1,461 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/thanhpk/randstr"
|
||||
)
|
||||
|
||||
const (
|
||||
PaymentMethodCreem = "creem"
|
||||
CreemSignatureHeader = "creem-signature"
|
||||
)
|
||||
|
||||
var creemAdaptor = &CreemAdaptor{}
|
||||
|
||||
// 生成HMAC-SHA256签名
|
||||
func generateCreemSignature(payload string, secret string) string {
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
h.Write([]byte(payload))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// 验证Creem webhook签名
|
||||
func verifyCreemSignature(payload string, signature string, secret string) bool {
|
||||
if secret == "" {
|
||||
log.Printf("Creem webhook secret not set")
|
||||
if setting.CreemTestMode {
|
||||
log.Printf("Skip Creem webhook sign verify in test mode")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
expectedSignature := generateCreemSignature(payload, secret)
|
||||
return hmac.Equal([]byte(signature), []byte(expectedSignature))
|
||||
}
|
||||
|
||||
type CreemPayRequest struct {
|
||||
ProductId string `json:"product_id"`
|
||||
PaymentMethod string `json:"payment_method"`
|
||||
}
|
||||
|
||||
type CreemProduct struct {
|
||||
ProductId string `json:"productId"`
|
||||
Name string `json:"name"`
|
||||
Price float64 `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
Quota int64 `json:"quota"`
|
||||
}
|
||||
|
||||
type CreemAdaptor struct {
|
||||
}
|
||||
|
||||
func (*CreemAdaptor) RequestPay(c *gin.Context, req *CreemPayRequest) {
|
||||
if req.PaymentMethod != PaymentMethodCreem {
|
||||
c.JSON(200, gin.H{"message": "error", "data": "不支持的支付渠道"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.ProductId == "" {
|
||||
c.JSON(200, gin.H{"message": "error", "data": "请选择产品"})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析产品列表
|
||||
var products []CreemProduct
|
||||
err := json.Unmarshal([]byte(setting.CreemProducts), &products)
|
||||
if err != nil {
|
||||
log.Println("解析Creem产品列表失败", err)
|
||||
c.JSON(200, gin.H{"message": "error", "data": "产品配置错误"})
|
||||
return
|
||||
}
|
||||
|
||||
// 查找对应的产品
|
||||
var selectedProduct *CreemProduct
|
||||
for _, product := range products {
|
||||
if product.ProductId == req.ProductId {
|
||||
selectedProduct = &product
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if selectedProduct == nil {
|
||||
c.JSON(200, gin.H{"message": "error", "data": "产品不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
id := c.GetInt("id")
|
||||
user, _ := model.GetUserById(id, false)
|
||||
|
||||
// 生成唯一的订单引用ID
|
||||
reference := fmt.Sprintf("creem-api-ref-%d-%d-%s", user.Id, time.Now().UnixMilli(), randstr.String(4))
|
||||
referenceId := "ref_" + common.Sha1([]byte(reference))
|
||||
|
||||
// 先创建订单记录,使用产品配置的金额和充值额度
|
||||
topUp := &model.TopUp{
|
||||
UserId: id,
|
||||
Amount: selectedProduct.Quota, // 充值额度
|
||||
Money: selectedProduct.Price, // 支付金额
|
||||
TradeNo: referenceId,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
err = topUp.Insert()
|
||||
if err != nil {
|
||||
log.Printf("创建Creem订单失败: %v", err)
|
||||
c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建支付链接,传入用户邮箱
|
||||
checkoutUrl, err := genCreemLink(referenceId, selectedProduct, user.Email, user.Username)
|
||||
if err != nil {
|
||||
log.Printf("获取Creem支付链接失败: %v", err)
|
||||
c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Creem订单创建成功 - 用户ID: %d, 订单号: %s, 产品: %s, 充值额度: %d, 支付金额: %.2f",
|
||||
id, referenceId, selectedProduct.Name, selectedProduct.Quota, selectedProduct.Price)
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"message": "success",
|
||||
"data": gin.H{
|
||||
"checkout_url": checkoutUrl,
|
||||
"order_id": referenceId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func RequestCreemPay(c *gin.Context) {
|
||||
var req CreemPayRequest
|
||||
|
||||
// 读取body内容用于打印,同时保留原始数据供后续使用
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
log.Printf("read creem pay req body err: %v", err)
|
||||
c.JSON(200, gin.H{"message": "error", "data": "read query error"})
|
||||
return
|
||||
}
|
||||
|
||||
// 打印body内容
|
||||
log.Printf("creem pay request body: %s", string(bodyBytes))
|
||||
|
||||
// 重新设置body供后续的ShouldBindJSON使用
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
|
||||
err = c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
|
||||
return
|
||||
}
|
||||
creemAdaptor.RequestPay(c, &req)
|
||||
}
|
||||
|
||||
// 新的Creem Webhook结构体,匹配实际的webhook数据格式
|
||||
type CreemWebhookEvent struct {
|
||||
Id string `json:"id"`
|
||||
EventType string `json:"eventType"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Object struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
RequestId string `json:"request_id"`
|
||||
Order struct {
|
||||
Object string `json:"object"`
|
||||
Id string `json:"id"`
|
||||
Customer string `json:"customer"`
|
||||
Product string `json:"product"`
|
||||
Amount int `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
SubTotal int `json:"sub_total"`
|
||||
TaxAmount int `json:"tax_amount"`
|
||||
AmountDue int `json:"amount_due"`
|
||||
AmountPaid int `json:"amount_paid"`
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
Transaction string `json:"transaction"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Mode string `json:"mode"`
|
||||
} `json:"order"`
|
||||
Product struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Price int `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
BillingType string `json:"billing_type"`
|
||||
BillingPeriod string `json:"billing_period"`
|
||||
Status string `json:"status"`
|
||||
TaxMode string `json:"tax_mode"`
|
||||
TaxCategory string `json:"tax_category"`
|
||||
DefaultSuccessUrl *string `json:"default_success_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Mode string `json:"mode"`
|
||||
} `json:"product"`
|
||||
Units int `json:"units"`
|
||||
Customer struct {
|
||||
Id string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Country string `json:"country"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Mode string `json:"mode"`
|
||||
} `json:"customer"`
|
||||
Status string `json:"status"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
Mode string `json:"mode"`
|
||||
} `json:"object"`
|
||||
}
|
||||
|
||||
// 保留旧的结构体作为兼容
|
||||
type CreemWebhookData struct {
|
||||
Type string `json:"type"`
|
||||
Data struct {
|
||||
RequestId string `json:"request_id"`
|
||||
Status string `json:"status"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func CreemWebhook(c *gin.Context) {
|
||||
// 读取body内容用于打印,同时保留原始数据供后续使用
|
||||
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
log.Printf("读取Creem Webhook请求body失败: %v", err)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取签名头
|
||||
signature := c.GetHeader(CreemSignatureHeader)
|
||||
|
||||
// 打印关键信息(避免输出完整敏感payload)
|
||||
log.Printf("Creem Webhook - URI: %s", c.Request.RequestURI)
|
||||
if setting.CreemTestMode {
|
||||
log.Printf("Creem Webhook - Signature: %s , Body: %s", signature, bodyBytes)
|
||||
} else if signature == "" {
|
||||
log.Printf("Creem Webhook缺少签名头")
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
if !verifyCreemSignature(string(bodyBytes), signature, setting.CreemWebhookSecret) {
|
||||
log.Printf("Creem Webhook签名验证失败")
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Creem Webhook签名验证成功")
|
||||
|
||||
// 重新设置body供后续的ShouldBindJSON使用
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||
|
||||
// 解析新格式的webhook数据
|
||||
var webhookEvent CreemWebhookEvent
|
||||
if err := c.ShouldBindJSON(&webhookEvent); err != nil {
|
||||
log.Printf("解析Creem Webhook参数失败: %v", err)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Creem Webhook解析成功 - EventType: %s, EventId: %s", webhookEvent.EventType, webhookEvent.Id)
|
||||
|
||||
// 根据事件类型处理不同的webhook
|
||||
switch webhookEvent.EventType {
|
||||
case "checkout.completed":
|
||||
handleCheckoutCompleted(c, &webhookEvent)
|
||||
default:
|
||||
log.Printf("忽略Creem Webhook事件类型: %s", webhookEvent.EventType)
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理支付完成事件
|
||||
func handleCheckoutCompleted(c *gin.Context, event *CreemWebhookEvent) {
|
||||
// 验证订单状态
|
||||
if event.Object.Order.Status != "paid" {
|
||||
log.Printf("订单状态不是已支付: %s, 跳过处理", event.Object.Order.Status)
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取引用ID(这是我们创建订单时传递的request_id)
|
||||
referenceId := event.Object.RequestId
|
||||
if referenceId == "" {
|
||||
log.Println("Creem Webhook缺少request_id字段")
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证订单类型,目前只处理一次性付款
|
||||
if event.Object.Order.Type != "onetime" {
|
||||
log.Printf("暂不支持的订单类型: %s, 跳过处理", event.Object.Order.Type)
|
||||
c.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录详细的支付信息
|
||||
log.Printf("处理Creem支付完成 - 订单号: %s, Creem订单ID: %s, 支付金额: %d %s, 客户邮箱: <redacted>, 产品: %s",
|
||||
referenceId,
|
||||
event.Object.Order.Id,
|
||||
event.Object.Order.AmountPaid,
|
||||
event.Object.Order.Currency,
|
||||
event.Object.Product.Name)
|
||||
|
||||
// 查询本地订单确认存在
|
||||
topUp := model.GetTopUpByTradeNo(referenceId)
|
||||
if topUp == nil {
|
||||
log.Printf("Creem充值订单不存在: %s", referenceId)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if topUp.Status != common.TopUpStatusPending {
|
||||
log.Printf("Creem充值订单状态错误: %s, 当前状态: %s", referenceId, topUp.Status)
|
||||
c.Status(http.StatusOK) // 已处理过的订单,返回成功避免重复处理
|
||||
return
|
||||
}
|
||||
|
||||
// 处理充值,传入客户邮箱和姓名信息
|
||||
customerEmail := event.Object.Customer.Email
|
||||
customerName := event.Object.Customer.Name
|
||||
|
||||
// 防护性检查,确保邮箱和姓名不为空字符串
|
||||
if customerEmail == "" {
|
||||
log.Printf("警告:Creem回调中客户邮箱为空 - 订单号: %s", referenceId)
|
||||
}
|
||||
if customerName == "" {
|
||||
log.Printf("警告:Creem回调中客户姓名为空 - 订单号: %s", referenceId)
|
||||
}
|
||||
|
||||
err := model.RechargeCreem(referenceId, customerEmail, customerName)
|
||||
if err != nil {
|
||||
log.Printf("Creem充值处理失败: %s, 订单号: %s", err.Error(), referenceId)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Creem充值成功 - 订单号: %s, 充值额度: %d, 支付金额: %.2f",
|
||||
referenceId, topUp.Amount, topUp.Money)
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
type CreemCheckoutRequest struct {
|
||||
ProductId string `json:"product_id"`
|
||||
RequestId string `json:"request_id"`
|
||||
Customer struct {
|
||||
Email string `json:"email"`
|
||||
} `json:"customer"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type CreemCheckoutResponse struct {
|
||||
CheckoutUrl string `json:"checkout_url"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
func genCreemLink(referenceId string, product *CreemProduct, email string, username string) (string, error) {
|
||||
if setting.CreemApiKey == "" {
|
||||
return "", fmt.Errorf("未配置Creem API密钥")
|
||||
}
|
||||
|
||||
// 根据测试模式选择 API 端点
|
||||
apiUrl := "https://api.creem.io/v1/checkouts"
|
||||
if setting.CreemTestMode {
|
||||
apiUrl = "https://test-api.creem.io/v1/checkouts"
|
||||
log.Printf("使用Creem测试环境: %s", apiUrl)
|
||||
}
|
||||
|
||||
// 构建请求数据,确保包含用户邮箱
|
||||
requestData := CreemCheckoutRequest{
|
||||
ProductId: product.ProductId,
|
||||
RequestId: referenceId, // 这个作为订单ID传递给Creem
|
||||
Customer: struct {
|
||||
Email string `json:"email"`
|
||||
}{
|
||||
Email: email, // 用户邮箱会在支付页面预填充
|
||||
},
|
||||
Metadata: map[string]string{
|
||||
"username": username,
|
||||
"reference_id": referenceId,
|
||||
"product_name": product.Name,
|
||||
"quota": fmt.Sprintf("%d", product.Quota),
|
||||
},
|
||||
}
|
||||
|
||||
// 序列化请求数据
|
||||
jsonData, err := json.Marshal(requestData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("序列化请求数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建 HTTP 请求
|
||||
req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建HTTP请求失败: %v", err)
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("x-api-key", setting.CreemApiKey)
|
||||
|
||||
log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 用户邮箱: %s, 订单号: %s",
|
||||
apiUrl, product.ProductId, email, referenceId)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("发送HTTP请求失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Creem API resp - status code: %d, resp: %s", resp.StatusCode, string(body))
|
||||
|
||||
// 检查响应状态
|
||||
if resp.StatusCode/100 != 2 {
|
||||
return "", fmt.Errorf("Creem API http status %d ", resp.StatusCode)
|
||||
}
|
||||
// 解析响应
|
||||
var checkoutResp CreemCheckoutResponse
|
||||
err = json.Unmarshal(body, &checkoutResp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if checkoutResp.CheckoutUrl == "" {
|
||||
return "", fmt.Errorf("Creem API resp no checkout url ")
|
||||
}
|
||||
|
||||
log.Printf("Creem 支付链接创建成功 - 订单号: %s, 支付链接: %s", referenceId, checkoutResp.CheckoutUrl)
|
||||
return checkoutResp.CheckoutUrl, nil
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
@@ -579,7 +580,7 @@ func GetUserModels(c *gin.Context) {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
groups := setting.GetUserUsableGroups(user.Group)
|
||||
groups := service.GetUserUsableGroups(user.Group)
|
||||
var models []string
|
||||
for group := range groups {
|
||||
for _, g := range model.GetGroupEnabledModels(group) {
|
||||
|
||||
15
dto/audio.go
15
dto/audio.go
@@ -1,17 +1,22 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AudioRequest struct {
|
||||
Model string `json:"model"`
|
||||
Input string `json:"input"`
|
||||
Voice string `json:"voice"`
|
||||
Speed float64 `json:"speed,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Input string `json:"input"`
|
||||
Voice string `json:"voice"`
|
||||
Instructions string `json:"instructions,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
Speed float64 `json:"speed,omitempty"`
|
||||
StreamFormat string `json:"stream_format,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (r *AudioRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
|
||||
@@ -27,7 +27,8 @@ type ImageRequest struct {
|
||||
OutputCompression json.RawMessage `json:"output_compression,omitempty"`
|
||||
PartialImages json.RawMessage `json:"partial_images,omitempty"`
|
||||
// Stream bool `json:"stream,omitempty"`
|
||||
Watermark *bool `json:"watermark,omitempty"`
|
||||
Watermark *bool `json:"watermark,omitempty"`
|
||||
Image json.RawMessage `json:"image,omitempty"`
|
||||
// 用匿名参数接收额外参数
|
||||
Extra map[string]json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
13
go.mod
13
go.mod
@@ -5,6 +5,7 @@ go 1.25.1
|
||||
|
||||
require (
|
||||
github.com/Calcium-Ion/go-epay v0.0.4
|
||||
github.com/abema/go-mp4 v1.4.1
|
||||
github.com/andybalholm/brotli v1.1.1
|
||||
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0
|
||||
github.com/aws/aws-sdk-go-v2 v1.37.2
|
||||
@@ -18,24 +19,30 @@ require (
|
||||
github.com/gin-contrib/static v0.0.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/glebarez/sqlite v1.9.0
|
||||
github.com/go-audio/aiff v1.1.0
|
||||
github.com/go-audio/wav v1.1.0
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/go-webauthn/webauthn v0.14.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/jfreymuth/oggvorbis v1.0.5
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mewkiz/flac v1.0.13
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/samber/lo v1.39.0
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stripe/stripe-go/v81 v81.4.0
|
||||
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
|
||||
github.com/thanhpk/randstr v1.0.6
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
github.com/tiktoken-go/tokenizer v0.6.2
|
||||
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/net v0.43.0
|
||||
@@ -62,6 +69,8 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-audio/audio v1.0.0 // indirect
|
||||
github.com/go-audio/riff v1.0.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
@@ -73,16 +82,20 @@ require (
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/icza/bitio v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.1 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jfreymuth/vorbis v1.0.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d // indirect
|
||||
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
|
||||
40
go.sum
40
go.sum
@@ -1,5 +1,7 @@
|
||||
github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
|
||||
github.com/Calcium-Ion/go-epay v0.0.4/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
|
||||
github.com/abema/go-mp4 v1.4.1 h1:YoS4VRqd+pAmddRPLFf8vMk74kuGl6ULSjzhsIqwr6M=
|
||||
github.com/abema/go-mp4 v1.4.1/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+KcxaMk1lfrRnwCd1UUuOjJM/lri5eM1qMs=
|
||||
@@ -33,6 +35,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -67,6 +70,15 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
|
||||
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
|
||||
github.com/go-audio/aiff v1.1.0 h1:m2LYgu/2BarpF2yZnFPWtY3Tp41k0A4y51gDRZZsEuU=
|
||||
github.com/go-audio/aiff v1.1.0/go.mod h1:sDik1muYvhPiccClfri0fv6U2fyH/dy4VRWmUz0cz9Q=
|
||||
github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4=
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
|
||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
||||
github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g=
|
||||
github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
@@ -108,6 +120,7 @@ github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
@@ -118,6 +131,10 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0=
|
||||
github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k=
|
||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -126,6 +143,10 @@ github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
||||
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jfreymuth/oggvorbis v1.0.5 h1:u+Ck+R0eLSRhgq8WTmffYnrVtSztJcYrl588DM4e3kQ=
|
||||
github.com/jfreymuth/oggvorbis v1.0.5/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
|
||||
github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE=
|
||||
github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@@ -145,6 +166,7 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
@@ -152,10 +174,17 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21/go.mod h1:LlQmBGkOuV/SKzEDXBPKauvN2UqCgzXO2XjecTGj40s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mewkiz/flac v1.0.13 h1:6wF8rRQKBFW159Daqx6Ro7K5ZnlVhHUKfS5aTsC4oXs=
|
||||
github.com/mewkiz/flac v1.0.13/go.mod h1:HfPYDA+oxjyuqMu2V+cyKcxF51KM6incpw5eZXmfA6k=
|
||||
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d h1:IL2tii4jXLdhCeQN69HNzYYW1kl0meSG0wt5+sLwszU=
|
||||
github.com/mewkiz/pkg v0.0.0-20250417130911-3f050ff8c56d/go.mod h1:SIpumAnUWSy0q9RzKD3pyH3g1t5vdawUAPcW5tQrUtI=
|
||||
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985 h1:h8O1byDZ1uk6RUXMhj1QJU3VXFKXHDZxr4TXRPGeBa8=
|
||||
github.com/mewpkg/term v0.0.0-20241026122259-37a80af23985/go.mod h1:uiPmbdUbdt1NkGApKl7htQjZ8S7XaGUAVulJUJ9v6q4=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -170,6 +199,8 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
|
||||
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
@@ -209,6 +240,9 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stripe/stripe-go/v81 v81.4.0 h1:AuD9XzdAvl193qUCSaLocf8H+nRopOouXhxqJUzCLbw=
|
||||
github.com/stripe/stripe-go/v81 v81.4.0/go.mod h1:C/F4jlmnGNacvYtBp/LUHCvVUJEZffFQCobkzwY1WOo=
|
||||
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
|
||||
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI=
|
||||
github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI=
|
||||
github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o=
|
||||
github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -238,6 +272,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c h1:xA2TJS9Hu/ivzaZIrDcwvpJ3Fnpsk5fDOJ4iSnL6J0w=
|
||||
github.com/yapingcat/gomedia v0.0.0-20240906162731-17feea57090c/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
@@ -257,6 +293,7 @@ golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -270,6 +307,7 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -286,6 +324,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -66,7 +66,8 @@ func LogError(ctx context.Context, msg string) {
|
||||
logHelper(ctx, loggerError, msg)
|
||||
}
|
||||
|
||||
func LogDebug(ctx context.Context, msg string) {
|
||||
func LogDebug(ctx context.Context, msg string, args ...any) {
|
||||
msg = fmt.Sprintf(msg, args...)
|
||||
if common.DebugEnabled {
|
||||
logHelper(ctx, loggerDebug, msg)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
@@ -266,8 +266,8 @@ func TokenAuth() func(c *gin.Context) {
|
||||
tokenGroup := token.Group
|
||||
if tokenGroup != "" {
|
||||
// check common.UserUsableGroups[userGroup]
|
||||
if _, ok := setting.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("令牌分组 %s 已被禁用", tokenGroup))
|
||||
if _, ok := service.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("无权访问 %s 分组", tokenGroup))
|
||||
return
|
||||
}
|
||||
// check group in common.GroupRatio
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,7 +15,6 @@ import (
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
relayconstant "github.com/QuantumNous/new-api/relay/constant"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
@@ -79,27 +79,27 @@ func Distribute() func(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
var selectGroup string
|
||||
userGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
|
||||
usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
|
||||
// check path is /pg/chat/completions
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/pg/chat/completions") {
|
||||
playgroundRequest := &dto.PlayGroundRequest{}
|
||||
err = common.UnmarshalBodyReusable(c, playgroundRequest)
|
||||
if err != nil {
|
||||
abortWithOpenAiMessage(c, http.StatusBadRequest, "无效的请求, "+err.Error())
|
||||
abortWithOpenAiMessage(c, http.StatusBadRequest, "无效的playground请求, "+err.Error())
|
||||
return
|
||||
}
|
||||
if playgroundRequest.Group != "" {
|
||||
if !setting.GroupInUserUsableGroups(playgroundRequest.Group) && playgroundRequest.Group != userGroup {
|
||||
if !service.GroupInUserUsableGroups(usingGroup, playgroundRequest.Group) && playgroundRequest.Group != usingGroup {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, "无权访问该分组")
|
||||
return
|
||||
}
|
||||
userGroup = playgroundRequest.Group
|
||||
usingGroup = playgroundRequest.Group
|
||||
}
|
||||
}
|
||||
channel, selectGroup, err = model.CacheGetRandomSatisfiedChannel(c, userGroup, modelRequest.Model, 0)
|
||||
channel, selectGroup, err = service.CacheGetRandomSatisfiedChannel(c, usingGroup, modelRequest.Model, 0)
|
||||
if err != nil {
|
||||
showGroup := userGroup
|
||||
if userGroup == "auto" {
|
||||
showGroup := usingGroup
|
||||
if usingGroup == "auto" {
|
||||
showGroup = fmt.Sprintf("auto(%s)", selectGroup)
|
||||
}
|
||||
message := fmt.Sprintf("获取分组 %s 下模型 %s 的可用渠道失败(distributor): %s", showGroup, modelRequest.Model, err.Error())
|
||||
@@ -112,7 +112,7 @@ func Distribute() func(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if channel == nil {
|
||||
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, fmt.Sprintf("分组 %s 下模型 %s 无可用渠道(distributor)", userGroup, modelRequest.Model), string(types.ErrorCodeModelNotFound))
|
||||
abortWithOpenAiMessage(c, http.StatusServiceUnavailable, fmt.Sprintf("分组 %s 下模型 %s 无可用渠道(distributor)", usingGroup, modelRequest.Model), string(types.ErrorCodeModelNotFound))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,20 @@ func Distribute() func(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// getModelFromRequest 从请求中读取模型信息
|
||||
// 根据 Content-Type 自动处理:
|
||||
// - application/json
|
||||
// - application/x-www-form-urlencoded
|
||||
// - multipart/form-data
|
||||
func getModelFromRequest(c *gin.Context) (*ModelRequest, error) {
|
||||
var modelRequest ModelRequest
|
||||
err := common.UnmarshalBodyReusable(c, &modelRequest)
|
||||
if err != nil {
|
||||
return nil, errors.New("无效的请求, " + err.Error())
|
||||
}
|
||||
return &modelRequest, nil
|
||||
}
|
||||
|
||||
func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
var modelRequest ModelRequest
|
||||
shouldSelectChannel := true
|
||||
@@ -138,7 +152,7 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
midjourneyRequest := dto.MidjourneyRequest{}
|
||||
err = common.UnmarshalBodyReusable(c, &midjourneyRequest)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
return nil, false, errors.New("无效的midjourney请求, " + err.Error())
|
||||
}
|
||||
midjourneyModel, mjErr, success := service.GetMjRequestModel(relayMode, &midjourneyRequest)
|
||||
if mjErr != nil {
|
||||
@@ -175,23 +189,12 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
relayMode := relayconstant.RelayModeUnknown
|
||||
if c.Request.Method == http.MethodPost {
|
||||
relayMode = relayconstant.RelayModeVideoSubmit
|
||||
contentType := c.Request.Header.Get("Content-Type")
|
||||
if strings.HasPrefix(contentType, "multipart/form-data") {
|
||||
form, err := common.ParseMultipartFormReusable(c)
|
||||
if err != nil {
|
||||
return nil, false, errors.New("无效的video请求, " + err.Error())
|
||||
}
|
||||
defer form.RemoveAll()
|
||||
if form != nil {
|
||||
if values, ok := form.Value["model"]; ok && len(values) > 0 {
|
||||
modelRequest.Model = values[0]
|
||||
}
|
||||
}
|
||||
} else if strings.HasPrefix(contentType, "application/json") {
|
||||
err = common.UnmarshalBodyReusable(c, &modelRequest)
|
||||
if err != nil {
|
||||
return nil, false, errors.New("无效的video请求, " + err.Error())
|
||||
}
|
||||
req, err := getModelFromRequest(c)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if req != nil {
|
||||
modelRequest.Model = req.Model
|
||||
}
|
||||
} else if c.Request.Method == http.MethodGet {
|
||||
relayMode = relayconstant.RelayModeVideoFetchByID
|
||||
@@ -201,10 +204,11 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
} else if strings.Contains(c.Request.URL.Path, "/v1/video/generations") {
|
||||
relayMode := relayconstant.RelayModeUnknown
|
||||
if c.Request.Method == http.MethodPost {
|
||||
err = common.UnmarshalBodyReusable(c, &modelRequest)
|
||||
req, err := getModelFromRequest(c)
|
||||
if err != nil {
|
||||
return nil, false, errors.New("video无效的请求, " + err.Error())
|
||||
return nil, false, err
|
||||
}
|
||||
modelRequest.Model = req.Model
|
||||
relayMode = relayconstant.RelayModeVideoSubmit
|
||||
} else if c.Request.Method == http.MethodGet {
|
||||
relayMode = relayconstant.RelayModeVideoFetchByID
|
||||
@@ -222,10 +226,11 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
}
|
||||
c.Set("relay_mode", relayMode)
|
||||
} else if !strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") && !strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
err = common.UnmarshalBodyReusable(c, &modelRequest)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, errors.New("无效的请求, " + err.Error())
|
||||
req, err := getModelFromRequest(c)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
modelRequest.Model = req.Model
|
||||
}
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/realtime") {
|
||||
//wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01
|
||||
@@ -245,20 +250,31 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, "dall-e")
|
||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/edits") {
|
||||
//modelRequest.Model = common.GetStringIfEmpty(c.PostForm("model"), "gpt-image-1")
|
||||
if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
|
||||
modelRequest.Model = c.PostForm("model")
|
||||
contentType := c.ContentType()
|
||||
if slices.Contains([]string{gin.MIMEPOSTForm, gin.MIMEMultipartPOSTForm}, contentType) {
|
||||
req, err := getModelFromRequest(c)
|
||||
if err == nil && req.Model != "" {
|
||||
modelRequest.Model = req.Model
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/audio") {
|
||||
relayMode := relayconstant.RelayModeAudioSpeech
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/audio/speech") {
|
||||
|
||||
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, "tts-1")
|
||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/audio/translations") {
|
||||
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, c.PostForm("model"))
|
||||
// 先尝试从请求读取
|
||||
if req, err := getModelFromRequest(c); err == nil && req.Model != "" {
|
||||
modelRequest.Model = req.Model
|
||||
}
|
||||
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, "whisper-1")
|
||||
relayMode = relayconstant.RelayModeAudioTranslation
|
||||
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") {
|
||||
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, c.PostForm("model"))
|
||||
// 先尝试从请求读取
|
||||
if req, err := getModelFromRequest(c); err == nil && req.Model != "" {
|
||||
modelRequest.Model = req.Model
|
||||
}
|
||||
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, "whisper-1")
|
||||
relayMode = relayconstant.RelayModeAudioTranscription
|
||||
}
|
||||
@@ -266,10 +282,12 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
|
||||
}
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/pg/chat/completions") {
|
||||
// playground chat completions
|
||||
err = common.UnmarshalBodyReusable(c, &modelRequest)
|
||||
req, err := getModelFromRequest(c)
|
||||
if err != nil {
|
||||
return nil, false, errors.New("无效的请求, " + err.Error())
|
||||
return nil, false, err
|
||||
}
|
||||
modelRequest.Model = req.Model
|
||||
modelRequest.Group = req.Group
|
||||
common.SetContextKey(c, constant.ContextKeyTokenGroup, modelRequest.Group)
|
||||
}
|
||||
return &modelRequest, shouldSelectChannel, nil
|
||||
|
||||
@@ -103,7 +103,7 @@ func getChannelQuery(group string, model string, retry int) (*gorm.DB, error) {
|
||||
return channelQuery, nil
|
||||
}
|
||||
|
||||
func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) {
|
||||
func GetChannel(group string, model string, retry int) (*Channel, error) {
|
||||
var abilities []Ability
|
||||
|
||||
var err error = nil
|
||||
|
||||
@@ -11,10 +11,7 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var group2model2channels map[string]map[string][]int // enabled channel
|
||||
@@ -96,43 +93,10 @@ func SyncChannelCache(frequency int) {
|
||||
}
|
||||
}
|
||||
|
||||
func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, model string, retry int) (*Channel, string, error) {
|
||||
var channel *Channel
|
||||
var err error
|
||||
selectGroup := group
|
||||
if group == "auto" {
|
||||
if len(setting.AutoGroups) == 0 {
|
||||
return nil, selectGroup, errors.New("auto groups is not enabled")
|
||||
}
|
||||
for _, autoGroup := range setting.AutoGroups {
|
||||
if common.DebugEnabled {
|
||||
println("autoGroup:", autoGroup)
|
||||
}
|
||||
channel, _ = getRandomSatisfiedChannel(autoGroup, model, retry)
|
||||
if channel == nil {
|
||||
continue
|
||||
} else {
|
||||
c.Set("auto_group", autoGroup)
|
||||
selectGroup = autoGroup
|
||||
if common.DebugEnabled {
|
||||
println("selectGroup:", selectGroup)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel, err = getRandomSatisfiedChannel(group, model, retry)
|
||||
if err != nil {
|
||||
return nil, group, err
|
||||
}
|
||||
}
|
||||
return channel, selectGroup, nil
|
||||
}
|
||||
|
||||
func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) {
|
||||
func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) {
|
||||
// if memory cache is disabled, get channel directly from database
|
||||
if !common.MemoryCacheEnabled {
|
||||
return GetRandomSatisfiedChannel(group, model, retry)
|
||||
return GetChannel(group, model, retry)
|
||||
}
|
||||
|
||||
channelSyncLock.RLock()
|
||||
@@ -178,30 +142,36 @@ func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
|
||||
targetPriority := int64(sortedUniquePriorities[retry])
|
||||
|
||||
// get the priority for the given retry number
|
||||
var shouldSmooth = false
|
||||
var sumWeight = 0
|
||||
var targetChannels []*Channel
|
||||
for _, channelId := range channels {
|
||||
if channel, ok := channelsIDM[channelId]; ok {
|
||||
if channel.GetPriority() == targetPriority {
|
||||
sumWeight += channel.GetWeight()
|
||||
targetChannels = append(targetChannels, channel)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId)
|
||||
}
|
||||
}
|
||||
if sumWeight/len(targetChannels) < 10 {
|
||||
shouldSmooth = true
|
||||
}
|
||||
|
||||
// 平滑系数
|
||||
smoothingFactor := 10
|
||||
// Calculate the total weight of all channels up to endIdx
|
||||
totalWeight := 0
|
||||
for _, channel := range targetChannels {
|
||||
totalWeight += channel.GetWeight() + smoothingFactor
|
||||
smoothingFactor := 1
|
||||
if shouldSmooth {
|
||||
smoothingFactor = 100
|
||||
}
|
||||
// Calculate the total weight of all channels up to endIdx
|
||||
totalWeight := sumWeight * smoothingFactor
|
||||
// Generate a random value in the range [0, totalWeight)
|
||||
randomWeight := rand.Intn(totalWeight)
|
||||
|
||||
// Find a channel based on its weight
|
||||
for _, channel := range targetChannels {
|
||||
randomWeight -= channel.GetWeight() + smoothingFactor
|
||||
randomWeight -= channel.GetWeight() * smoothingFactor
|
||||
if randomWeight < 0 {
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ func InitOptionMap() {
|
||||
common.OptionMap["StripePriceId"] = setting.StripePriceId
|
||||
common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64)
|
||||
common.OptionMap["StripePromotionCodesEnabled"] = strconv.FormatBool(setting.StripePromotionCodesEnabled)
|
||||
common.OptionMap["CreemApiKey"] = setting.CreemApiKey
|
||||
common.OptionMap["CreemProducts"] = setting.CreemProducts
|
||||
common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode)
|
||||
common.OptionMap["CreemWebhookSecret"] = setting.CreemWebhookSecret
|
||||
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
|
||||
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
||||
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
|
||||
@@ -342,6 +346,14 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
setting.StripeMinTopUp, _ = strconv.Atoi(value)
|
||||
case "StripePromotionCodesEnabled":
|
||||
setting.StripePromotionCodesEnabled = value == "true"
|
||||
case "CreemApiKey":
|
||||
setting.CreemApiKey = value
|
||||
case "CreemProducts":
|
||||
setting.CreemProducts = value
|
||||
case "CreemTestMode":
|
||||
setting.CreemTestMode = value == "true"
|
||||
case "CreemWebhookSecret":
|
||||
setting.CreemWebhookSecret = value
|
||||
case "TopupGroupRatio":
|
||||
err = common.UpdateTopupGroupRatioByJSONString(value)
|
||||
case "GitHubClientId":
|
||||
|
||||
@@ -305,3 +305,72 @@ func ManualCompleteTopUp(tradeNo string) error {
|
||||
RecordLog(userId, LogTypeTopup, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney))
|
||||
return nil
|
||||
}
|
||||
func RechargeCreem(referenceId string, customerEmail string, customerName string) (err error) {
|
||||
if referenceId == "" {
|
||||
return errors.New("未提供支付单号")
|
||||
}
|
||||
|
||||
var quota int64
|
||||
topUp := &TopUp{}
|
||||
|
||||
refCol := "`trade_no`"
|
||||
if common.UsingPostgreSQL {
|
||||
refCol = `"trade_no"`
|
||||
}
|
||||
|
||||
err = DB.Transaction(func(tx *gorm.DB) error {
|
||||
err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", referenceId).First(topUp).Error
|
||||
if err != nil {
|
||||
return errors.New("充值订单不存在")
|
||||
}
|
||||
|
||||
if topUp.Status != common.TopUpStatusPending {
|
||||
return errors.New("充值订单状态错误")
|
||||
}
|
||||
|
||||
topUp.CompleteTime = common.GetTimestamp()
|
||||
topUp.Status = common.TopUpStatusSuccess
|
||||
err = tx.Save(topUp).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Creem 直接使用 Amount 作为充值额度(整数)
|
||||
quota = topUp.Amount
|
||||
|
||||
// 构建更新字段,优先使用邮箱,如果邮箱为空则使用用户名
|
||||
updateFields := map[string]interface{}{
|
||||
"quota": gorm.Expr("quota + ?", quota),
|
||||
}
|
||||
|
||||
// 如果有客户邮箱,尝试更新用户邮箱(仅当用户邮箱为空时)
|
||||
if customerEmail != "" {
|
||||
// 先检查用户当前邮箱是否为空
|
||||
var user User
|
||||
err = tx.Where("id = ?", topUp.UserId).First(&user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果用户邮箱为空,则更新为支付时使用的邮箱
|
||||
if user.Email == "" {
|
||||
updateFields["email"] = customerEmail
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Model(&User{}).Where("id = ?", topUp.UserId).Updates(updateFields).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.New("充值失败," + err.Error())
|
||||
}
|
||||
|
||||
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", quota, topUp.Money))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
package ali
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/relay/helper"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// https://help.aliyun.com/document_detail/613695.html?spm=a2c4g.2399480.0.0.1adb778fAdzP9w#341800c0f8w0r
|
||||
@@ -29,180 +16,3 @@ func requestOpenAI2Ali(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIReque
|
||||
}
|
||||
return &request
|
||||
}
|
||||
|
||||
func embeddingRequestOpenAI2Ali(request dto.EmbeddingRequest) *AliEmbeddingRequest {
|
||||
return &AliEmbeddingRequest{
|
||||
Model: request.Model,
|
||||
Input: struct {
|
||||
Texts []string `json:"texts"`
|
||||
}{
|
||||
Texts: request.ParseInput(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func aliEmbeddingHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
|
||||
var fullTextResponse dto.FlexibleEmbeddingResponse
|
||||
err := json.NewDecoder(resp.Body).Decode(&fullTextResponse)
|
||||
if err != nil {
|
||||
return types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError), nil
|
||||
}
|
||||
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
|
||||
model := c.GetString("model")
|
||||
if model == "" {
|
||||
model = "text-embedding-v4"
|
||||
}
|
||||
jsonResponse, err := json.Marshal(fullTextResponse)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
}
|
||||
c.Writer.Header().Set("Content-Type", "application/json")
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
c.Writer.Write(jsonResponse)
|
||||
return nil, &fullTextResponse.Usage
|
||||
}
|
||||
|
||||
func embeddingResponseAli2OpenAI(response *AliEmbeddingResponse, model string) *dto.OpenAIEmbeddingResponse {
|
||||
openAIEmbeddingResponse := dto.OpenAIEmbeddingResponse{
|
||||
Object: "list",
|
||||
Data: make([]dto.OpenAIEmbeddingResponseItem, 0, len(response.Output.Embeddings)),
|
||||
Model: model,
|
||||
Usage: dto.Usage{TotalTokens: response.Usage.TotalTokens},
|
||||
}
|
||||
|
||||
for _, item := range response.Output.Embeddings {
|
||||
openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, dto.OpenAIEmbeddingResponseItem{
|
||||
Object: `embedding`,
|
||||
Index: item.TextIndex,
|
||||
Embedding: item.Embedding,
|
||||
})
|
||||
}
|
||||
return &openAIEmbeddingResponse
|
||||
}
|
||||
|
||||
func responseAli2OpenAI(response *AliResponse) *dto.OpenAITextResponse {
|
||||
choice := dto.OpenAITextResponseChoice{
|
||||
Index: 0,
|
||||
Message: dto.Message{
|
||||
Role: "assistant",
|
||||
Content: response.Output.Text,
|
||||
},
|
||||
FinishReason: response.Output.FinishReason,
|
||||
}
|
||||
fullTextResponse := dto.OpenAITextResponse{
|
||||
Id: response.RequestId,
|
||||
Object: "chat.completion",
|
||||
Created: common.GetTimestamp(),
|
||||
Choices: []dto.OpenAITextResponseChoice{choice},
|
||||
Usage: dto.Usage{
|
||||
PromptTokens: response.Usage.InputTokens,
|
||||
CompletionTokens: response.Usage.OutputTokens,
|
||||
TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens,
|
||||
},
|
||||
}
|
||||
return &fullTextResponse
|
||||
}
|
||||
|
||||
func streamResponseAli2OpenAI(aliResponse *AliResponse) *dto.ChatCompletionsStreamResponse {
|
||||
var choice dto.ChatCompletionsStreamResponseChoice
|
||||
choice.Delta.SetContentString(aliResponse.Output.Text)
|
||||
if aliResponse.Output.FinishReason != "null" {
|
||||
finishReason := aliResponse.Output.FinishReason
|
||||
choice.FinishReason = &finishReason
|
||||
}
|
||||
response := dto.ChatCompletionsStreamResponse{
|
||||
Id: aliResponse.RequestId,
|
||||
Object: "chat.completion.chunk",
|
||||
Created: common.GetTimestamp(),
|
||||
Model: "ernie-bot",
|
||||
Choices: []dto.ChatCompletionsStreamResponseChoice{choice},
|
||||
}
|
||||
return &response
|
||||
}
|
||||
|
||||
func aliStreamHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
|
||||
var usage dto.Usage
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
dataChan := make(chan string)
|
||||
stopChan := make(chan bool)
|
||||
go func() {
|
||||
for scanner.Scan() {
|
||||
data := scanner.Text()
|
||||
if len(data) < 5 { // ignore blank line or wrong format
|
||||
continue
|
||||
}
|
||||
if data[:5] != "data:" {
|
||||
continue
|
||||
}
|
||||
data = data[5:]
|
||||
dataChan <- data
|
||||
}
|
||||
stopChan <- true
|
||||
}()
|
||||
helper.SetEventStreamHeaders(c)
|
||||
lastResponseText := ""
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
select {
|
||||
case data := <-dataChan:
|
||||
var aliResponse AliResponse
|
||||
err := json.Unmarshal([]byte(data), &aliResponse)
|
||||
if err != nil {
|
||||
common.SysLog("error unmarshalling stream response: " + err.Error())
|
||||
return true
|
||||
}
|
||||
if aliResponse.Usage.OutputTokens != 0 {
|
||||
usage.PromptTokens = aliResponse.Usage.InputTokens
|
||||
usage.CompletionTokens = aliResponse.Usage.OutputTokens
|
||||
usage.TotalTokens = aliResponse.Usage.InputTokens + aliResponse.Usage.OutputTokens
|
||||
}
|
||||
response := streamResponseAli2OpenAI(&aliResponse)
|
||||
response.Choices[0].Delta.SetContentString(strings.TrimPrefix(response.Choices[0].Delta.GetContentString(), lastResponseText))
|
||||
lastResponseText = aliResponse.Output.Text
|
||||
jsonResponse, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
common.SysLog("error marshalling stream response: " + err.Error())
|
||||
return true
|
||||
}
|
||||
c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
|
||||
return true
|
||||
case <-stopChan:
|
||||
c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
|
||||
return false
|
||||
}
|
||||
})
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
return nil, &usage
|
||||
}
|
||||
|
||||
func aliHandler(c *gin.Context, resp *http.Response) (*types.NewAPIError, *dto.Usage) {
|
||||
var aliResponse AliResponse
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError), nil
|
||||
}
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
err = json.Unmarshal(responseBody, &aliResponse)
|
||||
if err != nil {
|
||||
return types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError), nil
|
||||
}
|
||||
if aliResponse.Code != "" {
|
||||
return types.WithOpenAIError(types.OpenAIError{
|
||||
Message: aliResponse.Message,
|
||||
Type: "ali_error",
|
||||
Param: aliResponse.RequestId,
|
||||
Code: aliResponse.Code,
|
||||
}, resp.StatusCode), nil
|
||||
}
|
||||
fullTextResponse := responseAli2OpenAI(&aliResponse)
|
||||
jsonResponse, err := common.Marshal(fullTextResponse)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeBadResponseBody), nil
|
||||
}
|
||||
c.Writer.Header().Set("Content-Type", "application/json")
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
_, err = c.Writer.Write(jsonResponse)
|
||||
return nil, &fullTextResponse.Usage
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ var awsModelIDMap = map[string]string{
|
||||
"claude-opus-4-20250514": "anthropic.claude-opus-4-20250514-v1:0",
|
||||
"claude-opus-4-1-20250805": "anthropic.claude-opus-4-1-20250805-v1:0",
|
||||
"claude-sonnet-4-5-20250929": "anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
"claude-haiku-4-5-20251001": "anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
// Nova models
|
||||
"nova-micro-v1:0": "amazon.nova-micro-v1:0",
|
||||
"nova-lite-v1:0": "amazon.nova-lite-v1:0",
|
||||
@@ -75,6 +76,11 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{
|
||||
"ap": true,
|
||||
"eu": true,
|
||||
},
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0": {
|
||||
"us": true,
|
||||
"ap": true,
|
||||
"eu": true,
|
||||
},
|
||||
// Nova models - all support three major regions
|
||||
"amazon.nova-micro-v1:0": {
|
||||
"us": true,
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/QuantumNous/new-api/relay/channel/claude"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/relay/helper"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -25,6 +26,19 @@ import (
|
||||
)
|
||||
|
||||
func newAwsClient(c *gin.Context, info *relaycommon.RelayInfo) (*bedrockruntime.Client, error) {
|
||||
var (
|
||||
httpClient *http.Client
|
||||
err error
|
||||
)
|
||||
if info.ChannelSetting.Proxy != "" {
|
||||
httpClient, err = service.NewProxyHttpClient(info.ChannelSetting.Proxy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new proxy http client failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
httpClient = service.GetHttpClient()
|
||||
}
|
||||
|
||||
awsSecret := strings.Split(info.ApiKey, "|")
|
||||
var client *bedrockruntime.Client
|
||||
switch len(awsSecret) {
|
||||
@@ -34,6 +48,7 @@ func newAwsClient(c *gin.Context, info *relaycommon.RelayInfo) (*bedrockruntime.
|
||||
client = bedrockruntime.New(bedrockruntime.Options{
|
||||
Region: region,
|
||||
BearerAuthTokenProvider: bearer.StaticTokenProvider{Token: bearer.Token{Value: apiKey}},
|
||||
HTTPClient: httpClient,
|
||||
})
|
||||
case 3:
|
||||
ak := awsSecret[0]
|
||||
@@ -42,6 +57,7 @@ func newAwsClient(c *gin.Context, info *relaycommon.RelayInfo) (*bedrockruntime.
|
||||
client = bedrockruntime.New(bedrockruntime.Options{
|
||||
Region: region,
|
||||
Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(ak, sk, "")),
|
||||
HTTPClient: httpClient,
|
||||
})
|
||||
default:
|
||||
return nil, errors.New("invalid aws secret key")
|
||||
|
||||
132
relay/channel/minimax/adaptor.go
Normal file
132
relay/channel/minimax/adaptor.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package minimax
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/relay/channel"
|
||||
"github.com/QuantumNous/new-api/relay/channel/openai"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/relay/constant"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Adaptor struct {
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
|
||||
if info.RelayMode != constant.RelayModeAudioSpeech {
|
||||
return nil, errors.New("unsupported audio relay mode")
|
||||
}
|
||||
|
||||
voiceID := request.Voice
|
||||
speed := request.Speed
|
||||
outputFormat := request.ResponseFormat
|
||||
|
||||
minimaxRequest := MiniMaxTTSRequest{
|
||||
Model: info.OriginModelName,
|
||||
Text: request.Input,
|
||||
VoiceSetting: VoiceSetting{
|
||||
VoiceID: voiceID,
|
||||
Speed: speed,
|
||||
},
|
||||
AudioSetting: &AudioSetting{
|
||||
Format: outputFormat,
|
||||
},
|
||||
OutputFormat: outputFormat,
|
||||
}
|
||||
|
||||
// 同步扩展字段的厂商自定义metadata
|
||||
if len(request.Metadata) > 0 {
|
||||
if err := json.Unmarshal(request.Metadata, &minimaxRequest); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling metadata to minimax request: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(minimaxRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshalling minimax request: %w", err)
|
||||
}
|
||||
if outputFormat != "hex" {
|
||||
outputFormat = "url"
|
||||
}
|
||||
|
||||
c.Set("response_format", outputFormat)
|
||||
|
||||
// Debug: log the request structure
|
||||
// fmt.Printf("MiniMax TTS Request: %s\n", string(jsonData))
|
||||
|
||||
return bytes.NewReader(jsonData), nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
return GetRequestURL(info)
|
||||
}
|
||||
|
||||
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
||||
channel.SetupApiRequestHeader(info, c, req)
|
||||
req.Set("Authorization", "Bearer "+info.ApiKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
|
||||
if request == nil {
|
||||
return nil, errors.New("request is nil")
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
||||
return channel.DoApiRequest(a, c, info, requestBody)
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
|
||||
if info.RelayMode == constant.RelayModeAudioSpeech {
|
||||
return handleTTSResponse(c, resp, info)
|
||||
}
|
||||
|
||||
adaptor := openai.Adaptor{}
|
||||
return adaptor.DoResponse(c, resp, info)
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetModelList() []string {
|
||||
return ModelList
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetChannelName() string {
|
||||
return ChannelName
|
||||
}
|
||||
@@ -8,6 +8,12 @@ var ModelList = []string{
|
||||
"abab6-chat",
|
||||
"abab5.5-chat",
|
||||
"abab5.5s-chat",
|
||||
"speech-2.5-hd-preview",
|
||||
"speech-2.5-turbo-preview",
|
||||
"speech-02-hd",
|
||||
"speech-02-turbo",
|
||||
"speech-01-hd",
|
||||
"speech-01-turbo",
|
||||
}
|
||||
|
||||
var ChannelName = "minimax"
|
||||
|
||||
@@ -3,9 +3,23 @@ package minimax
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
channelconstant "github.com/QuantumNous/new-api/constant"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/relay/constant"
|
||||
)
|
||||
|
||||
func GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
return fmt.Sprintf("%s/v1/text/chatcompletion_v2", info.ChannelBaseUrl), nil
|
||||
baseUrl := info.ChannelBaseUrl
|
||||
if baseUrl == "" {
|
||||
baseUrl = channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeMiniMax]
|
||||
}
|
||||
|
||||
switch info.RelayMode {
|
||||
case constant.RelayModeChatCompletions:
|
||||
return fmt.Sprintf("%s/v1/text/chatcompletion_v2", baseUrl), nil
|
||||
case constant.RelayModeAudioSpeech:
|
||||
return fmt.Sprintf("%s/v1/t2a_v2", baseUrl), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported relay mode: %d", info.RelayMode)
|
||||
}
|
||||
}
|
||||
|
||||
194
relay/channel/minimax/tts.go
Normal file
194
relay/channel/minimax/tts.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package minimax
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MiniMaxTTSRequest struct {
|
||||
Model string `json:"model"`
|
||||
Text string `json:"text"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
||||
VoiceSetting VoiceSetting `json:"voice_setting"`
|
||||
PronunciationDict *PronunciationDict `json:"pronunciation_dict,omitempty"`
|
||||
AudioSetting *AudioSetting `json:"audio_setting,omitempty"`
|
||||
TimbreWeights []TimbreWeight `json:"timbre_weights,omitempty"`
|
||||
LanguageBoost string `json:"language_boost,omitempty"`
|
||||
VoiceModify *VoiceModify `json:"voice_modify,omitempty"`
|
||||
SubtitleEnable bool `json:"subtitle_enable,omitempty"`
|
||||
OutputFormat string `json:"output_format,omitempty"`
|
||||
AigcWatermark bool `json:"aigc_watermark,omitempty"`
|
||||
}
|
||||
|
||||
type StreamOptions struct {
|
||||
ExcludeAggregatedAudio bool `json:"exclude_aggregated_audio,omitempty"`
|
||||
}
|
||||
|
||||
type VoiceSetting struct {
|
||||
VoiceID string `json:"voice_id"`
|
||||
Speed float64 `json:"speed,omitempty"`
|
||||
Vol float64 `json:"vol,omitempty"`
|
||||
Pitch int `json:"pitch,omitempty"`
|
||||
Emotion string `json:"emotion,omitempty"`
|
||||
TextNormalization bool `json:"text_normalization,omitempty"`
|
||||
LatexRead bool `json:"latex_read,omitempty"`
|
||||
}
|
||||
|
||||
type PronunciationDict struct {
|
||||
Tone []string `json:"tone,omitempty"`
|
||||
}
|
||||
|
||||
type AudioSetting struct {
|
||||
SampleRate int `json:"sample_rate,omitempty"`
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
Channel int `json:"channel,omitempty"`
|
||||
ForceCbr bool `json:"force_cbr,omitempty"`
|
||||
}
|
||||
|
||||
type TimbreWeight struct {
|
||||
VoiceID string `json:"voice_id"`
|
||||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
type VoiceModify struct {
|
||||
Pitch int `json:"pitch,omitempty"`
|
||||
Intensity int `json:"intensity,omitempty"`
|
||||
Timbre int `json:"timbre,omitempty"`
|
||||
SoundEffects string `json:"sound_effects,omitempty"`
|
||||
}
|
||||
|
||||
type MiniMaxTTSResponse struct {
|
||||
Data MiniMaxTTSData `json:"data"`
|
||||
ExtraInfo MiniMaxExtraInfo `json:"extra_info"`
|
||||
TraceID string `json:"trace_id"`
|
||||
BaseResp MiniMaxBaseResp `json:"base_resp"`
|
||||
}
|
||||
|
||||
type MiniMaxTTSData struct {
|
||||
Audio string `json:"audio"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
type MiniMaxExtraInfo struct {
|
||||
UsageCharacters int64 `json:"usage_characters"`
|
||||
}
|
||||
|
||||
type MiniMaxBaseResp struct {
|
||||
StatusCode int64 `json:"status_code"`
|
||||
StatusMsg string `json:"status_msg"`
|
||||
}
|
||||
|
||||
func getContentTypeByFormat(format string) string {
|
||||
contentTypeMap := map[string]string{
|
||||
"mp3": "audio/mpeg",
|
||||
"wav": "audio/wav",
|
||||
"flac": "audio/flac",
|
||||
"aac": "audio/aac",
|
||||
"pcm": "audio/pcm",
|
||||
}
|
||||
if ct, ok := contentTypeMap[format]; ok {
|
||||
return ct
|
||||
}
|
||||
return "audio/mpeg" // default to mp3
|
||||
}
|
||||
|
||||
func handleTTSResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
if readErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to read minimax response: %w", readErr),
|
||||
types.ErrorCodeReadResponseBodyFailed,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Parse response
|
||||
var minimaxResp MiniMaxTTSResponse
|
||||
if unmarshalErr := json.Unmarshal(body, &minimaxResp); unmarshalErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to unmarshal minimax TTS response: %w", unmarshalErr),
|
||||
types.ErrorCodeBadResponseBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
// Check base_resp status code
|
||||
if minimaxResp.BaseResp.StatusCode != 0 {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("minimax TTS error: %d - %s", minimaxResp.BaseResp.StatusCode, minimaxResp.BaseResp.StatusMsg),
|
||||
types.ErrorCodeBadResponse,
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
}
|
||||
|
||||
// Check if we have audio data
|
||||
if minimaxResp.Data.Audio == "" {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("no audio data in minimax TTS response"),
|
||||
types.ErrorCodeBadResponse,
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(minimaxResp.Data.Audio, "http") {
|
||||
c.Redirect(http.StatusFound, minimaxResp.Data.Audio)
|
||||
} else {
|
||||
// Handle hex-encoded audio data
|
||||
audioData, decodeErr := hex.DecodeString(minimaxResp.Data.Audio)
|
||||
if decodeErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to decode hex audio data: %w", decodeErr),
|
||||
types.ErrorCodeBadResponse,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
// Determine content type - default to mp3
|
||||
contentType := "audio/mpeg"
|
||||
|
||||
c.Data(http.StatusOK, contentType, audioData)
|
||||
}
|
||||
|
||||
usage = &dto.Usage{
|
||||
PromptTokens: info.PromptTokens,
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: int(minimaxResp.ExtraInfo.UsageCharacters),
|
||||
}
|
||||
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
func handleChatCompletionResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
if readErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New("failed to read minimax response"),
|
||||
types.ErrorCodeReadResponseBodyFailed,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Set response headers
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
c.Data(resp.StatusCode, "application/json", body)
|
||||
return nil, nil
|
||||
}
|
||||
@@ -121,7 +121,14 @@ func ollamaStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
|
||||
if chunk.Message != nil && len(chunk.Message.Thinking) > 0 {
|
||||
raw := strings.TrimSpace(string(chunk.Message.Thinking))
|
||||
if raw != "" && raw != "null" {
|
||||
delta.Choices[0].Delta.SetReasoningContent(raw)
|
||||
// Unmarshal the JSON string to get the actual content without quotes
|
||||
var thinkingContent string
|
||||
if err := json.Unmarshal(chunk.Message.Thinking, &thinkingContent); err == nil {
|
||||
delta.Choices[0].Delta.SetReasoningContent(thinkingContent)
|
||||
} else {
|
||||
// Fallback to raw string if it's not a JSON string
|
||||
delta.Choices[0].Delta.SetReasoningContent(raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
// tool calls
|
||||
@@ -209,7 +216,14 @@ func ollamaChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R
|
||||
if ck.Message != nil && len(ck.Message.Thinking) > 0 {
|
||||
raw := strings.TrimSpace(string(ck.Message.Thinking))
|
||||
if raw != "" && raw != "null" {
|
||||
reasoningBuilder.WriteString(raw)
|
||||
// Unmarshal the JSON string to get the actual content without quotes
|
||||
var thinkingContent string
|
||||
if err := json.Unmarshal(ck.Message.Thinking, &thinkingContent); err == nil {
|
||||
reasoningBuilder.WriteString(thinkingContent)
|
||||
} else {
|
||||
// Fallback to raw string if it's not a JSON string
|
||||
reasoningBuilder.WriteString(raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
if ck.Message != nil && ck.Message.Content != "" {
|
||||
@@ -229,7 +243,14 @@ func ollamaChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R
|
||||
if len(single.Message.Thinking) > 0 {
|
||||
raw := strings.TrimSpace(string(single.Message.Thinking))
|
||||
if raw != "" && raw != "null" {
|
||||
reasoningBuilder.WriteString(raw)
|
||||
// Unmarshal the JSON string to get the actual content without quotes
|
||||
var thinkingContent string
|
||||
if err := json.Unmarshal(single.Message.Thinking, &thinkingContent); err == nil {
|
||||
reasoningBuilder.WriteString(thinkingContent)
|
||||
} else {
|
||||
// Fallback to raw string if it's not a JSON string
|
||||
reasoningBuilder.WriteString(raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
aggContent.WriteString(single.Message.Content)
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"github.com/QuantumNous/new-api/relay/channel"
|
||||
"github.com/QuantumNous/new-api/relay/channel/ai360"
|
||||
"github.com/QuantumNous/new-api/relay/channel/lingyiwanwu"
|
||||
"github.com/QuantumNous/new-api/relay/channel/minimax"
|
||||
//"github.com/QuantumNous/new-api/relay/channel/minimax"
|
||||
"github.com/QuantumNous/new-api/relay/channel/openrouter"
|
||||
"github.com/QuantumNous/new-api/relay/channel/xinference"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
@@ -161,8 +161,8 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
requestURL = fmt.Sprintf("/openai/realtime?deployment=%s&api-version=%s", model_, apiVersion)
|
||||
}
|
||||
return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, requestURL, info.ChannelType), nil
|
||||
case constant.ChannelTypeMiniMax:
|
||||
return minimax.GetRequestURL(info)
|
||||
//case constant.ChannelTypeMiniMax:
|
||||
// return minimax.GetRequestURL(info)
|
||||
case constant.ChannelTypeCustom:
|
||||
url := info.ChannelBaseUrl
|
||||
url = strings.Replace(url, "{model}", info.UpstreamModelName, -1)
|
||||
@@ -599,8 +599,8 @@ func (a *Adaptor) GetModelList() []string {
|
||||
return ai360.ModelList
|
||||
case constant.ChannelTypeLingYiWanWu:
|
||||
return lingyiwanwu.ModelList
|
||||
case constant.ChannelTypeMiniMax:
|
||||
return minimax.ModelList
|
||||
//case constant.ChannelTypeMiniMax:
|
||||
// return minimax.ModelList
|
||||
case constant.ChannelTypeXinference:
|
||||
return xinference.ModelList
|
||||
case constant.ChannelTypeOpenRouter:
|
||||
@@ -616,8 +616,8 @@ func (a *Adaptor) GetChannelName() string {
|
||||
return ai360.ChannelName
|
||||
case constant.ChannelTypeLingYiWanWu:
|
||||
return lingyiwanwu.ChannelName
|
||||
case constant.ChannelTypeMiniMax:
|
||||
return minimax.ChannelName
|
||||
//case constant.ChannelTypeMiniMax:
|
||||
// return minimax.ChannelName
|
||||
case constant.ChannelTypeXinference:
|
||||
return xinference.ChannelName
|
||||
case constant.ChannelTypeOpenRouter:
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
@@ -26,7 +21,6 @@ import (
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error {
|
||||
@@ -273,6 +267,39 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
|
||||
return &simpleResponse.Usage, nil
|
||||
}
|
||||
|
||||
func streamTTSResponse(c *gin.Context, resp *http.Response) {
|
||||
c.Writer.WriteHeaderNow()
|
||||
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
logger.LogWarn(c, "streaming not supported")
|
||||
_, err := io.Copy(c.Writer, resp.Body)
|
||||
if err != nil {
|
||||
logger.LogWarn(c, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
buffer := make([]byte, 4096)
|
||||
for {
|
||||
n, err := resp.Body.Read(buffer)
|
||||
//logger.LogInfo(c, fmt.Sprintf("streamTTSResponse read %d bytes", n))
|
||||
if n > 0 {
|
||||
if _, writeErr := c.Writer.Write(buffer[:n]); writeErr != nil {
|
||||
logger.LogError(c, writeErr.Error())
|
||||
break
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
logger.LogError(c, err.Error())
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func OpenaiTTSHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) *dto.Usage {
|
||||
// the status code has been judged before, if there is a body reading failure,
|
||||
// it should be regarded as a non-recoverable error, so it should not return err for external retry.
|
||||
@@ -288,10 +315,16 @@ func OpenaiTTSHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
||||
c.Writer.Header().Set(k, v[0])
|
||||
}
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
c.Writer.WriteHeaderNow()
|
||||
_, err := io.Copy(c.Writer, resp.Body)
|
||||
if err != nil {
|
||||
logger.LogError(c, err.Error())
|
||||
|
||||
isStreaming := resp.ContentLength == -1 || resp.Header.Get("Content-Length") == ""
|
||||
if isStreaming {
|
||||
streamTTSResponse(c, resp)
|
||||
} else {
|
||||
c.Writer.WriteHeaderNow()
|
||||
_, err := io.Copy(c.Writer, resp.Body)
|
||||
if err != nil {
|
||||
logger.LogError(c, err.Error())
|
||||
}
|
||||
}
|
||||
return usage
|
||||
}
|
||||
@@ -322,59 +355,13 @@ func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
||||
}
|
||||
}
|
||||
|
||||
audioTokens, err := countAudioTokens(c)
|
||||
if err != nil {
|
||||
return types.NewError(err, types.ErrorCodeCountTokenFailed), nil
|
||||
}
|
||||
usage := &dto.Usage{}
|
||||
usage.PromptTokens = audioTokens
|
||||
usage.PromptTokens = info.PromptTokens
|
||||
usage.CompletionTokens = 0
|
||||
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
|
||||
return nil, usage
|
||||
}
|
||||
|
||||
func countAudioTokens(c *gin.Context) (int, error) {
|
||||
body, err := common.GetRequestBody(c)
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
var reqBody struct {
|
||||
File *multipart.FileHeader `form:"file" binding:"required"`
|
||||
}
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(body))
|
||||
if err = c.ShouldBind(&reqBody); err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
ext := filepath.Ext(reqBody.File.Filename) // 获取文件扩展名
|
||||
reqFp, err := reqBody.File.Open()
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
defer reqFp.Close()
|
||||
|
||||
tmpFp, err := os.CreateTemp("", "audio-*"+ext)
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
defer os.Remove(tmpFp.Name())
|
||||
|
||||
_, err = io.Copy(tmpFp, reqFp)
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
if err = tmpFp.Close(); err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
duration, err := common.GetAudioDuration(c.Request.Context(), tmpFp.Name(), ext)
|
||||
if err != nil {
|
||||
return 0, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return int(math.Round(math.Ceil(duration) / 60.0 * 1000)), nil // 1 minute 相当于 1k tokens
|
||||
}
|
||||
|
||||
func OpenaiRealtimeHandler(c *gin.Context, info *relaycommon.RelayInfo) (*types.NewAPIError, *dto.RealtimeUsage) {
|
||||
if info == nil || info.ClientWs == nil || info.TargetWs == nil {
|
||||
return types.NewError(fmt.Errorf("invalid websocket connection"), types.ErrorCodeBadResponse), nil
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -302,6 +303,29 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
|
||||
return ti, nil
|
||||
}
|
||||
|
||||
func (a *TaskAdaptor) ConvertToOpenAIVideo(task *model.Task) ([]byte, error) {
|
||||
upstreamName, err := decodeLocalTaskID(task.TaskID)
|
||||
if err != nil {
|
||||
upstreamName = ""
|
||||
}
|
||||
modelName := extractModelFromOperationName(upstreamName)
|
||||
if strings.TrimSpace(modelName) == "" {
|
||||
modelName = "veo-3.0-generate-001"
|
||||
}
|
||||
v := dto.NewOpenAIVideo()
|
||||
v.ID = task.TaskID
|
||||
v.Model = modelName
|
||||
v.Status = task.Status.ToVideoStatus()
|
||||
v.SetProgressStr(task.Progress)
|
||||
v.CreatedAt = task.CreatedAt
|
||||
v.CompletedAt = task.UpdatedAt
|
||||
if strings.HasPrefix(task.FailReason, "data:") && len(task.FailReason) > 0 {
|
||||
v.SetMetadata("url", task.FailReason)
|
||||
}
|
||||
|
||||
return common.Marshal(v)
|
||||
}
|
||||
|
||||
// ============================
|
||||
// helpers
|
||||
// ============================
|
||||
|
||||
@@ -6,9 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -23,6 +21,11 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
contextKeyTTSRequest = "volcengine_tts_request"
|
||||
contextKeyResponseFormat = "response_format"
|
||||
)
|
||||
|
||||
type Adaptor struct {
|
||||
}
|
||||
|
||||
@@ -37,136 +40,175 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
|
||||
//TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
if info.RelayMode != constant.RelayModeAudioSpeech {
|
||||
return nil, errors.New("unsupported audio relay mode")
|
||||
}
|
||||
|
||||
appID, token, err := parseVolcengineAuth(info.ApiKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
voiceType := mapVoiceType(request.Voice)
|
||||
speedRatio := request.Speed
|
||||
encoding := mapEncoding(request.ResponseFormat)
|
||||
|
||||
c.Set(contextKeyResponseFormat, encoding)
|
||||
|
||||
volcRequest := VolcengineTTSRequest{
|
||||
App: VolcengineTTSApp{
|
||||
AppID: appID,
|
||||
Token: token,
|
||||
Cluster: "volcano_tts",
|
||||
},
|
||||
User: VolcengineTTSUser{
|
||||
UID: "openai_relay_user",
|
||||
},
|
||||
Audio: VolcengineTTSAudio{
|
||||
VoiceType: voiceType,
|
||||
Encoding: encoding,
|
||||
SpeedRatio: speedRatio,
|
||||
Rate: 24000,
|
||||
},
|
||||
Request: VolcengineTTSReqInfo{
|
||||
ReqID: generateRequestID(),
|
||||
Text: request.Input,
|
||||
Operation: "submit",
|
||||
Model: info.OriginModelName,
|
||||
},
|
||||
}
|
||||
|
||||
if len(request.Metadata) > 0 {
|
||||
if err = json.Unmarshal(request.Metadata, &volcRequest); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling metadata to volcengine request: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(contextKeyTTSRequest, volcRequest)
|
||||
|
||||
if volcRequest.Request.Operation == "submit" {
|
||||
info.IsStream = true
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(volcRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshalling volcengine request: %w", err)
|
||||
}
|
||||
|
||||
return bytes.NewReader(jsonData), nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
|
||||
switch info.RelayMode {
|
||||
case constant.RelayModeImagesGenerations:
|
||||
return request, nil
|
||||
case constant.RelayModeImagesEdits:
|
||||
|
||||
var requestBody bytes.Buffer
|
||||
writer := multipart.NewWriter(&requestBody)
|
||||
|
||||
writer.WriteField("model", request.Model)
|
||||
// 获取所有表单字段
|
||||
formData := c.Request.PostForm
|
||||
// 遍历表单字段并打印输出
|
||||
for key, values := range formData {
|
||||
if key == "model" {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
writer.WriteField(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the multipart form to handle both single image and multiple images
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max memory
|
||||
return nil, errors.New("failed to parse multipart form")
|
||||
}
|
||||
|
||||
if c.Request.MultipartForm != nil && c.Request.MultipartForm.File != nil {
|
||||
// Check if "image" field exists in any form, including array notation
|
||||
var imageFiles []*multipart.FileHeader
|
||||
var exists bool
|
||||
|
||||
// First check for standard "image" field
|
||||
if imageFiles, exists = c.Request.MultipartForm.File["image"]; !exists || len(imageFiles) == 0 {
|
||||
// If not found, check for "image[]" field
|
||||
if imageFiles, exists = c.Request.MultipartForm.File["image[]"]; !exists || len(imageFiles) == 0 {
|
||||
// If still not found, iterate through all fields to find any that start with "image["
|
||||
foundArrayImages := false
|
||||
for fieldName, files := range c.Request.MultipartForm.File {
|
||||
if strings.HasPrefix(fieldName, "image[") && len(files) > 0 {
|
||||
foundArrayImages = true
|
||||
for _, file := range files {
|
||||
imageFiles = append(imageFiles, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no image fields found at all
|
||||
if !foundArrayImages && (len(imageFiles) == 0) {
|
||||
return nil, errors.New("image is required")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process all image files
|
||||
for i, fileHeader := range imageFiles {
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open image file %d: %w", i, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// If multiple images, use image[] as the field name
|
||||
fieldName := "image"
|
||||
if len(imageFiles) > 1 {
|
||||
fieldName = "image[]"
|
||||
}
|
||||
|
||||
// Determine MIME type based on file extension
|
||||
mimeType := detectImageMimeType(fileHeader.Filename)
|
||||
|
||||
// Create a form file with the appropriate content type
|
||||
h := make(textproto.MIMEHeader)
|
||||
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldName, fileHeader.Filename))
|
||||
h.Set("Content-Type", mimeType)
|
||||
|
||||
part, err := writer.CreatePart(h)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create form part failed for image %d: %w", i, err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, file); err != nil {
|
||||
return nil, fmt.Errorf("copy file failed for image %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle mask file if present
|
||||
if maskFiles, exists := c.Request.MultipartForm.File["mask"]; exists && len(maskFiles) > 0 {
|
||||
maskFile, err := maskFiles[0].Open()
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to open mask file")
|
||||
}
|
||||
defer maskFile.Close()
|
||||
|
||||
// Determine MIME type for mask file
|
||||
mimeType := detectImageMimeType(maskFiles[0].Filename)
|
||||
|
||||
// Create a form file with the appropriate content type
|
||||
h := make(textproto.MIMEHeader)
|
||||
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="mask"; filename="%s"`, maskFiles[0].Filename))
|
||||
h.Set("Content-Type", mimeType)
|
||||
|
||||
maskPart, err := writer.CreatePart(h)
|
||||
if err != nil {
|
||||
return nil, errors.New("create form file failed for mask")
|
||||
}
|
||||
|
||||
if _, err := io.Copy(maskPart, maskFile); err != nil {
|
||||
return nil, errors.New("copy mask file failed")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("no multipart form data found")
|
||||
}
|
||||
|
||||
// 关闭 multipart 编写器以设置分界线
|
||||
writer.Close()
|
||||
c.Request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
return bytes.NewReader(requestBody.Bytes()), nil
|
||||
// 根据官方文档,并没有发现豆包生图支持表单请求:https://www.volcengine.com/docs/82379/1824121
|
||||
//case constant.RelayModeImagesEdits:
|
||||
//
|
||||
// var requestBody bytes.Buffer
|
||||
// writer := multipart.NewWriter(&requestBody)
|
||||
//
|
||||
// writer.WriteField("model", request.Model)
|
||||
//
|
||||
// formData := c.Request.PostForm
|
||||
// for key, values := range formData {
|
||||
// if key == "model" {
|
||||
// continue
|
||||
// }
|
||||
// for _, value := range values {
|
||||
// writer.WriteField(key, value)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
|
||||
// return nil, errors.New("failed to parse multipart form")
|
||||
// }
|
||||
//
|
||||
// if c.Request.MultipartForm != nil && c.Request.MultipartForm.File != nil {
|
||||
// var imageFiles []*multipart.FileHeader
|
||||
// var exists bool
|
||||
//
|
||||
// if imageFiles, exists = c.Request.MultipartForm.File["image"]; !exists || len(imageFiles) == 0 {
|
||||
// if imageFiles, exists = c.Request.MultipartForm.File["image[]"]; !exists || len(imageFiles) == 0 {
|
||||
// foundArrayImages := false
|
||||
// for fieldName, files := range c.Request.MultipartForm.File {
|
||||
// if strings.HasPrefix(fieldName, "image[") && len(files) > 0 {
|
||||
// foundArrayImages = true
|
||||
// for _, file := range files {
|
||||
// imageFiles = append(imageFiles, file)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if !foundArrayImages && (len(imageFiles) == 0) {
|
||||
// return nil, errors.New("image is required")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// for i, fileHeader := range imageFiles {
|
||||
// file, err := fileHeader.Open()
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("failed to open image file %d: %w", i, err)
|
||||
// }
|
||||
// defer file.Close()
|
||||
//
|
||||
// fieldName := "image"
|
||||
// if len(imageFiles) > 1 {
|
||||
// fieldName = "image[]"
|
||||
// }
|
||||
//
|
||||
// mimeType := detectImageMimeType(fileHeader.Filename)
|
||||
//
|
||||
// h := make(textproto.MIMEHeader)
|
||||
// h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldName, fileHeader.Filename))
|
||||
// h.Set("Content-Type", mimeType)
|
||||
//
|
||||
// part, err := writer.CreatePart(h)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("create form part failed for image %d: %w", i, err)
|
||||
// }
|
||||
//
|
||||
// if _, err := io.Copy(part, file); err != nil {
|
||||
// return nil, fmt.Errorf("copy file failed for image %d: %w", i, err)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if maskFiles, exists := c.Request.MultipartForm.File["mask"]; exists && len(maskFiles) > 0 {
|
||||
// maskFile, err := maskFiles[0].Open()
|
||||
// if err != nil {
|
||||
// return nil, errors.New("failed to open mask file")
|
||||
// }
|
||||
// defer maskFile.Close()
|
||||
//
|
||||
// mimeType := detectImageMimeType(maskFiles[0].Filename)
|
||||
//
|
||||
// h := make(textproto.MIMEHeader)
|
||||
// h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="mask"; filename="%s"`, maskFiles[0].Filename))
|
||||
// h.Set("Content-Type", mimeType)
|
||||
//
|
||||
// maskPart, err := writer.CreatePart(h)
|
||||
// if err != nil {
|
||||
// return nil, errors.New("create form file failed for mask")
|
||||
// }
|
||||
//
|
||||
// if _, err := io.Copy(maskPart, maskFile); err != nil {
|
||||
// return nil, errors.New("copy mask file failed")
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// return nil, errors.New("no multipart form data found")
|
||||
// }
|
||||
//
|
||||
// writer.Close()
|
||||
// c.Request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
// return bytes.NewReader(requestBody.Bytes()), nil
|
||||
|
||||
default:
|
||||
return request, nil
|
||||
}
|
||||
}
|
||||
|
||||
// detectImageMimeType determines the MIME type based on the file extension
|
||||
func detectImageMimeType(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
switch ext {
|
||||
@@ -177,11 +219,9 @@ func detectImageMimeType(filename string) string {
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
default:
|
||||
// Try to detect from extension if possible
|
||||
if strings.HasPrefix(ext, ".jp") {
|
||||
return "image/jpeg"
|
||||
}
|
||||
// Default to png as a fallback
|
||||
return "image/png"
|
||||
}
|
||||
}
|
||||
@@ -190,7 +230,6 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
// 支持自定义域名,如果未设置则使用默认域名
|
||||
baseUrl := info.ChannelBaseUrl
|
||||
if baseUrl == "" {
|
||||
baseUrl = channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine]
|
||||
@@ -211,12 +250,18 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
return fmt.Sprintf("%s/api/v3/chat/completions", baseUrl), nil
|
||||
case constant.RelayModeEmbeddings:
|
||||
return fmt.Sprintf("%s/api/v3/embeddings", baseUrl), nil
|
||||
case constant.RelayModeImagesGenerations:
|
||||
//豆包的图生图也走generations接口: https://www.volcengine.com/docs/82379/1824121
|
||||
case constant.RelayModeImagesGenerations, constant.RelayModeImagesEdits:
|
||||
return fmt.Sprintf("%s/api/v3/images/generations", baseUrl), nil
|
||||
case constant.RelayModeImagesEdits:
|
||||
return fmt.Sprintf("%s/api/v3/images/edits", baseUrl), nil
|
||||
//case constant.RelayModeImagesEdits:
|
||||
// return fmt.Sprintf("%s/api/v3/images/edits", baseUrl), nil
|
||||
case constant.RelayModeRerank:
|
||||
return fmt.Sprintf("%s/api/v3/rerank", baseUrl), nil
|
||||
case constant.RelayModeAudioSpeech:
|
||||
if baseUrl == channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine] {
|
||||
return "wss://openspeech.bytedance.com/api/v1/tts/ws_binary", nil
|
||||
}
|
||||
return fmt.Sprintf("%s/v1/audio/speech", baseUrl), nil
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -225,6 +270,18 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
|
||||
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
||||
channel.SetupApiRequestHeader(info, c, req)
|
||||
|
||||
if info.RelayMode == constant.RelayModeAudioSpeech {
|
||||
parts := strings.Split(info.ApiKey, "|")
|
||||
if len(parts) == 2 {
|
||||
req.Set("Authorization", "Bearer;"+parts[1])
|
||||
}
|
||||
req.Set("Content-Type", "application/json")
|
||||
return nil
|
||||
} else if info.RelayMode == constant.RelayModeImagesEdits {
|
||||
req.Set("Content-Type", gin.MIMEJSON)
|
||||
}
|
||||
|
||||
req.Set("Authorization", "Bearer "+info.ApiKey)
|
||||
return nil
|
||||
}
|
||||
@@ -233,7 +290,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
|
||||
if request == nil {
|
||||
return nil, errors.New("request is nil")
|
||||
}
|
||||
// 适配 方舟deepseek混合模型 的 thinking 后缀
|
||||
|
||||
if strings.HasSuffix(info.UpstreamModelName, "-thinking") && strings.HasPrefix(info.UpstreamModelName, "deepseek") {
|
||||
info.UpstreamModelName = strings.TrimSuffix(info.UpstreamModelName, "-thinking")
|
||||
request.Model = info.UpstreamModelName
|
||||
@@ -251,15 +308,61 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
|
||||
// TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
||||
if info.RelayMode == constant.RelayModeAudioSpeech {
|
||||
baseUrl := info.ChannelBaseUrl
|
||||
if baseUrl == "" {
|
||||
baseUrl = channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine]
|
||||
}
|
||||
|
||||
if baseUrl == channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine] {
|
||||
if info.IsStream {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return channel.DoApiRequest(a, c, info, requestBody)
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
|
||||
if info.RelayMode == constant.RelayModeAudioSpeech {
|
||||
encoding := mapEncoding(c.GetString(contextKeyResponseFormat))
|
||||
if info.IsStream {
|
||||
volcRequestInterface, exists := c.Get(contextKeyTTSRequest)
|
||||
if !exists {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New("volcengine TTS request not found in context"),
|
||||
types.ErrorCodeBadRequestBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
volcRequest, ok := volcRequestInterface.(VolcengineTTSRequest)
|
||||
if !ok {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New("invalid volcengine TTS request type"),
|
||||
types.ErrorCodeBadRequestBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
// Get the WebSocket URL
|
||||
requestURL, urlErr := a.GetRequestURL(info)
|
||||
if urlErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
urlErr,
|
||||
types.ErrorCodeBadRequestBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
return handleTTSWebSocketResponse(c, requestURL, volcRequest, info, encoding)
|
||||
}
|
||||
return handleTTSResponse(c, resp, info, encoding)
|
||||
}
|
||||
|
||||
adaptor := openai.Adaptor{}
|
||||
usage, err = adaptor.DoResponse(c, resp, info)
|
||||
return
|
||||
|
||||
533
relay/channel/volcengine/protocols.go
Normal file
533
relay/channel/volcengine/protocols.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package volcengine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type (
|
||||
EventType int32
|
||||
MsgType uint8
|
||||
MsgTypeFlagBits uint8
|
||||
VersionBits uint8
|
||||
HeaderSizeBits uint8
|
||||
SerializationBits uint8
|
||||
CompressionBits uint8
|
||||
)
|
||||
|
||||
const (
|
||||
MsgTypeFlagNoSeq MsgTypeFlagBits = 0
|
||||
MsgTypeFlagPositiveSeq MsgTypeFlagBits = 0b1
|
||||
MsgTypeFlagNegativeSeq MsgTypeFlagBits = 0b11
|
||||
MsgTypeFlagWithEvent MsgTypeFlagBits = 0b100
|
||||
)
|
||||
|
||||
const (
|
||||
Version1 VersionBits = iota + 1
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderSize4 HeaderSizeBits = iota + 1
|
||||
)
|
||||
|
||||
const (
|
||||
SerializationJSON SerializationBits = 0b1
|
||||
)
|
||||
|
||||
const (
|
||||
CompressionNone CompressionBits = 0
|
||||
)
|
||||
|
||||
const (
|
||||
MsgTypeFullClientRequest MsgType = 0b1
|
||||
MsgTypeAudioOnlyClient MsgType = 0b10
|
||||
MsgTypeFullServerResponse MsgType = 0b1001
|
||||
MsgTypeAudioOnlyServer MsgType = 0b1011
|
||||
MsgTypeFrontEndResultServer MsgType = 0b1100
|
||||
MsgTypeError MsgType = 0b1111
|
||||
)
|
||||
|
||||
func (t MsgType) String() string {
|
||||
switch t {
|
||||
case MsgTypeFullClientRequest:
|
||||
return "MsgType_FullClientRequest"
|
||||
case MsgTypeAudioOnlyClient:
|
||||
return "MsgType_AudioOnlyClient"
|
||||
case MsgTypeFullServerResponse:
|
||||
return "MsgType_FullServerResponse"
|
||||
case MsgTypeAudioOnlyServer:
|
||||
return "MsgType_AudioOnlyServer"
|
||||
case MsgTypeError:
|
||||
return "MsgType_Error"
|
||||
case MsgTypeFrontEndResultServer:
|
||||
return "MsgType_FrontEndResultServer"
|
||||
default:
|
||||
return fmt.Sprintf("MsgType_(%d)", t)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
EventType_None EventType = 0
|
||||
|
||||
EventType_StartConnection EventType = 1
|
||||
EventType_FinishConnection EventType = 2
|
||||
|
||||
EventType_ConnectionStarted EventType = 50
|
||||
EventType_ConnectionFailed EventType = 51
|
||||
EventType_ConnectionFinished EventType = 52
|
||||
|
||||
EventType_StartSession EventType = 100
|
||||
EventType_CancelSession EventType = 101
|
||||
EventType_FinishSession EventType = 102
|
||||
|
||||
EventType_SessionStarted EventType = 150
|
||||
EventType_SessionCanceled EventType = 151
|
||||
EventType_SessionFinished EventType = 152
|
||||
EventType_SessionFailed EventType = 153
|
||||
|
||||
EventType_UsageResponse EventType = 154
|
||||
|
||||
EventType_TaskRequest EventType = 200
|
||||
EventType_UpdateConfig EventType = 201
|
||||
|
||||
EventType_AudioMuted EventType = 250
|
||||
|
||||
EventType_SayHello EventType = 300
|
||||
|
||||
EventType_TTSSentenceStart EventType = 350
|
||||
EventType_TTSSentenceEnd EventType = 351
|
||||
EventType_TTSResponse EventType = 352
|
||||
EventType_TTSEnded EventType = 359
|
||||
EventType_PodcastRoundStart EventType = 360
|
||||
EventType_PodcastRoundResponse EventType = 361
|
||||
EventType_PodcastRoundEnd EventType = 362
|
||||
|
||||
EventType_ASRInfo EventType = 450
|
||||
EventType_ASRResponse EventType = 451
|
||||
EventType_ASREnded EventType = 459
|
||||
|
||||
EventType_ChatTTSText EventType = 500
|
||||
|
||||
EventType_ChatResponse EventType = 550
|
||||
EventType_ChatEnded EventType = 559
|
||||
|
||||
EventType_SourceSubtitleStart EventType = 650
|
||||
EventType_SourceSubtitleResponse EventType = 651
|
||||
EventType_SourceSubtitleEnd EventType = 652
|
||||
|
||||
EventType_TranslationSubtitleStart EventType = 653
|
||||
EventType_TranslationSubtitleResponse EventType = 654
|
||||
EventType_TranslationSubtitleEnd EventType = 655
|
||||
)
|
||||
|
||||
func (t EventType) String() string {
|
||||
switch t {
|
||||
case EventType_None:
|
||||
return "EventType_None"
|
||||
case EventType_StartConnection:
|
||||
return "EventType_StartConnection"
|
||||
case EventType_FinishConnection:
|
||||
return "EventType_FinishConnection"
|
||||
case EventType_ConnectionStarted:
|
||||
return "EventType_ConnectionStarted"
|
||||
case EventType_ConnectionFailed:
|
||||
return "EventType_ConnectionFailed"
|
||||
case EventType_ConnectionFinished:
|
||||
return "EventType_ConnectionFinished"
|
||||
case EventType_StartSession:
|
||||
return "EventType_StartSession"
|
||||
case EventType_CancelSession:
|
||||
return "EventType_CancelSession"
|
||||
case EventType_FinishSession:
|
||||
return "EventType_FinishSession"
|
||||
case EventType_SessionStarted:
|
||||
return "EventType_SessionStarted"
|
||||
case EventType_SessionCanceled:
|
||||
return "EventType_SessionCanceled"
|
||||
case EventType_SessionFinished:
|
||||
return "EventType_SessionFinished"
|
||||
case EventType_SessionFailed:
|
||||
return "EventType_SessionFailed"
|
||||
case EventType_UsageResponse:
|
||||
return "EventType_UsageResponse"
|
||||
case EventType_TaskRequest:
|
||||
return "EventType_TaskRequest"
|
||||
case EventType_UpdateConfig:
|
||||
return "EventType_UpdateConfig"
|
||||
case EventType_AudioMuted:
|
||||
return "EventType_AudioMuted"
|
||||
case EventType_SayHello:
|
||||
return "EventType_SayHello"
|
||||
case EventType_TTSSentenceStart:
|
||||
return "EventType_TTSSentenceStart"
|
||||
case EventType_TTSSentenceEnd:
|
||||
return "EventType_TTSSentenceEnd"
|
||||
case EventType_TTSResponse:
|
||||
return "EventType_TTSResponse"
|
||||
case EventType_TTSEnded:
|
||||
return "EventType_TTSEnded"
|
||||
case EventType_PodcastRoundStart:
|
||||
return "EventType_PodcastRoundStart"
|
||||
case EventType_PodcastRoundResponse:
|
||||
return "EventType_PodcastRoundResponse"
|
||||
case EventType_PodcastRoundEnd:
|
||||
return "EventType_PodcastRoundEnd"
|
||||
case EventType_ASRInfo:
|
||||
return "EventType_ASRInfo"
|
||||
case EventType_ASRResponse:
|
||||
return "EventType_ASRResponse"
|
||||
case EventType_ASREnded:
|
||||
return "EventType_ASREnded"
|
||||
case EventType_ChatTTSText:
|
||||
return "EventType_ChatTTSText"
|
||||
case EventType_ChatResponse:
|
||||
return "EventType_ChatResponse"
|
||||
case EventType_ChatEnded:
|
||||
return "EventType_ChatEnded"
|
||||
case EventType_SourceSubtitleStart:
|
||||
return "EventType_SourceSubtitleStart"
|
||||
case EventType_SourceSubtitleResponse:
|
||||
return "EventType_SourceSubtitleResponse"
|
||||
case EventType_SourceSubtitleEnd:
|
||||
return "EventType_SourceSubtitleEnd"
|
||||
case EventType_TranslationSubtitleStart:
|
||||
return "EventType_TranslationSubtitleStart"
|
||||
case EventType_TranslationSubtitleResponse:
|
||||
return "EventType_TranslationSubtitleResponse"
|
||||
case EventType_TranslationSubtitleEnd:
|
||||
return "EventType_TranslationSubtitleEnd"
|
||||
default:
|
||||
return fmt.Sprintf("EventType_(%d)", t)
|
||||
}
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
Version VersionBits
|
||||
HeaderSize HeaderSizeBits
|
||||
MsgType MsgType
|
||||
MsgTypeFlag MsgTypeFlagBits
|
||||
Serialization SerializationBits
|
||||
Compression CompressionBits
|
||||
|
||||
EventType EventType
|
||||
SessionID string
|
||||
ConnectID string
|
||||
Sequence int32
|
||||
ErrorCode uint32
|
||||
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func NewMessageFromBytes(data []byte) (*Message, error) {
|
||||
if len(data) < 3 {
|
||||
return nil, fmt.Errorf("data too short: expected at least 3 bytes, got %d", len(data))
|
||||
}
|
||||
|
||||
typeAndFlag := data[1]
|
||||
|
||||
msg, err := NewMessage(MsgType(typeAndFlag>>4), MsgTypeFlagBits(typeAndFlag&0b00001111))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := msg.Unmarshal(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func NewMessage(msgType MsgType, flag MsgTypeFlagBits) (*Message, error) {
|
||||
return &Message{
|
||||
MsgType: msgType,
|
||||
MsgTypeFlag: flag,
|
||||
Version: Version1,
|
||||
HeaderSize: HeaderSize4,
|
||||
Serialization: SerializationJSON,
|
||||
Compression: CompressionNone,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Message) String() string {
|
||||
switch m.MsgType {
|
||||
case MsgTypeAudioOnlyServer, MsgTypeAudioOnlyClient:
|
||||
if m.MsgTypeFlag == MsgTypeFlagPositiveSeq || m.MsgTypeFlag == MsgTypeFlagNegativeSeq {
|
||||
return fmt.Sprintf("%s, %s, Sequence: %d, PayloadSize: %d", m.MsgType, m.EventType, m.Sequence, len(m.Payload))
|
||||
}
|
||||
return fmt.Sprintf("%s, %s, PayloadSize: %d", m.MsgType, m.EventType, len(m.Payload))
|
||||
case MsgTypeError:
|
||||
return fmt.Sprintf("%s, %s, ErrorCode: %d, Payload: %s", m.MsgType, m.EventType, m.ErrorCode, string(m.Payload))
|
||||
default:
|
||||
if m.MsgTypeFlag == MsgTypeFlagPositiveSeq || m.MsgTypeFlag == MsgTypeFlagNegativeSeq {
|
||||
return fmt.Sprintf("%s, %s, Sequence: %d, Payload: %s",
|
||||
m.MsgType, m.EventType, m.Sequence, string(m.Payload))
|
||||
}
|
||||
return fmt.Sprintf("%s, %s, Payload: %s", m.MsgType, m.EventType, string(m.Payload))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Message) Marshal() ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
header := []uint8{
|
||||
uint8(m.Version)<<4 | uint8(m.HeaderSize),
|
||||
uint8(m.MsgType)<<4 | uint8(m.MsgTypeFlag),
|
||||
uint8(m.Serialization)<<4 | uint8(m.Compression),
|
||||
}
|
||||
|
||||
headerSize := 4 * int(m.HeaderSize)
|
||||
if padding := headerSize - len(header); padding > 0 {
|
||||
header = append(header, make([]uint8, padding)...)
|
||||
}
|
||||
|
||||
if err := binary.Write(buf, binary.BigEndian, header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writers, err := m.writers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, write := range writers {
|
||||
if err := write(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (m *Message) Unmarshal(data []byte) error {
|
||||
buf := bytes.NewBuffer(data)
|
||||
|
||||
versionAndHeaderSize, err := buf.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Version = VersionBits(versionAndHeaderSize >> 4)
|
||||
m.HeaderSize = HeaderSizeBits(versionAndHeaderSize & 0b00001111)
|
||||
|
||||
_, err = buf.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serializationCompression, err := buf.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Serialization = SerializationBits(serializationCompression & 0b11110000)
|
||||
m.Compression = CompressionBits(serializationCompression & 0b00001111)
|
||||
|
||||
headerSize := 4 * int(m.HeaderSize)
|
||||
readSize := 3
|
||||
if paddingSize := headerSize - readSize; paddingSize > 0 {
|
||||
if n, err := buf.Read(make([]byte, paddingSize)); err != nil || n < paddingSize {
|
||||
return fmt.Errorf("insufficient header bytes: expected %d, got %d", paddingSize, n)
|
||||
}
|
||||
}
|
||||
|
||||
readers, err := m.readers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, read := range readers {
|
||||
if err := read(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := buf.ReadByte(); err != io.EOF {
|
||||
return fmt.Errorf("unexpected data after message: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) writers() (writers []func(*bytes.Buffer) error, _ error) {
|
||||
if m.MsgTypeFlag == MsgTypeFlagWithEvent {
|
||||
writers = append(writers, m.writeEvent, m.writeSessionID)
|
||||
}
|
||||
|
||||
switch m.MsgType {
|
||||
case MsgTypeFullClientRequest, MsgTypeFullServerResponse, MsgTypeFrontEndResultServer, MsgTypeAudioOnlyClient, MsgTypeAudioOnlyServer:
|
||||
if m.MsgTypeFlag == MsgTypeFlagPositiveSeq || m.MsgTypeFlag == MsgTypeFlagNegativeSeq {
|
||||
writers = append(writers, m.writeSequence)
|
||||
}
|
||||
case MsgTypeError:
|
||||
writers = append(writers, m.writeErrorCode)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported message type: %d", m.MsgType)
|
||||
}
|
||||
|
||||
writers = append(writers, m.writePayload)
|
||||
return writers, nil
|
||||
}
|
||||
|
||||
func (m *Message) writeEvent(buf *bytes.Buffer) error {
|
||||
return binary.Write(buf, binary.BigEndian, m.EventType)
|
||||
}
|
||||
|
||||
func (m *Message) writeSessionID(buf *bytes.Buffer) error {
|
||||
switch m.EventType {
|
||||
case EventType_StartConnection, EventType_FinishConnection,
|
||||
EventType_ConnectionStarted, EventType_ConnectionFailed:
|
||||
return nil
|
||||
}
|
||||
|
||||
size := len(m.SessionID)
|
||||
if size > math.MaxUint32 {
|
||||
return fmt.Errorf("session ID size (%d) exceeds max(uint32)", size)
|
||||
}
|
||||
|
||||
if err := binary.Write(buf, binary.BigEndian, uint32(size)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf.WriteString(m.SessionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) writeSequence(buf *bytes.Buffer) error {
|
||||
return binary.Write(buf, binary.BigEndian, m.Sequence)
|
||||
}
|
||||
|
||||
func (m *Message) writeErrorCode(buf *bytes.Buffer) error {
|
||||
return binary.Write(buf, binary.BigEndian, m.ErrorCode)
|
||||
}
|
||||
|
||||
func (m *Message) writePayload(buf *bytes.Buffer) error {
|
||||
size := len(m.Payload)
|
||||
if size > math.MaxUint32 {
|
||||
return fmt.Errorf("payload size (%d) exceeds max(uint32)", size)
|
||||
}
|
||||
|
||||
if err := binary.Write(buf, binary.BigEndian, uint32(size)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf.Write(m.Payload)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) readers() (readers []func(*bytes.Buffer) error, _ error) {
|
||||
switch m.MsgType {
|
||||
case MsgTypeFullClientRequest, MsgTypeFullServerResponse, MsgTypeFrontEndResultServer, MsgTypeAudioOnlyClient, MsgTypeAudioOnlyServer:
|
||||
if m.MsgTypeFlag == MsgTypeFlagPositiveSeq || m.MsgTypeFlag == MsgTypeFlagNegativeSeq {
|
||||
readers = append(readers, m.readSequence)
|
||||
}
|
||||
case MsgTypeError:
|
||||
readers = append(readers, m.readErrorCode)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported message type: %d", m.MsgType)
|
||||
}
|
||||
|
||||
if m.MsgTypeFlag == MsgTypeFlagWithEvent {
|
||||
readers = append(readers, m.readEvent, m.readSessionID, m.readConnectID)
|
||||
}
|
||||
|
||||
readers = append(readers, m.readPayload)
|
||||
return readers, nil
|
||||
}
|
||||
|
||||
func (m *Message) readEvent(buf *bytes.Buffer) error {
|
||||
return binary.Read(buf, binary.BigEndian, &m.EventType)
|
||||
}
|
||||
|
||||
func (m *Message) readSessionID(buf *bytes.Buffer) error {
|
||||
switch m.EventType {
|
||||
case EventType_StartConnection, EventType_FinishConnection,
|
||||
EventType_ConnectionStarted, EventType_ConnectionFailed,
|
||||
EventType_ConnectionFinished:
|
||||
return nil
|
||||
}
|
||||
|
||||
var size uint32
|
||||
if err := binary.Read(buf, binary.BigEndian, &size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if size > 0 {
|
||||
m.SessionID = string(buf.Next(int(size)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) readConnectID(buf *bytes.Buffer) error {
|
||||
switch m.EventType {
|
||||
case EventType_ConnectionStarted, EventType_ConnectionFailed,
|
||||
EventType_ConnectionFinished:
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
var size uint32
|
||||
if err := binary.Read(buf, binary.BigEndian, &size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if size > 0 {
|
||||
m.ConnectID = string(buf.Next(int(size)))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Message) readSequence(buf *bytes.Buffer) error {
|
||||
return binary.Read(buf, binary.BigEndian, &m.Sequence)
|
||||
}
|
||||
|
||||
func (m *Message) readErrorCode(buf *bytes.Buffer) error {
|
||||
return binary.Read(buf, binary.BigEndian, &m.ErrorCode)
|
||||
}
|
||||
|
||||
func (m *Message) readPayload(buf *bytes.Buffer) error {
|
||||
var size uint32
|
||||
if err := binary.Read(buf, binary.BigEndian, &size); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if size > 0 {
|
||||
m.Payload = buf.Next(int(size))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReceiveMessage(conn *websocket.Conn) (*Message, error) {
|
||||
mt, frame, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mt != websocket.BinaryMessage && mt != websocket.TextMessage {
|
||||
return nil, fmt.Errorf("unexpected Websocket message type: %d", mt)
|
||||
}
|
||||
msg, err := NewMessageFromBytes(frame)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func FullClientRequest(conn *websocket.Conn, payload []byte) error {
|
||||
msg, err := NewMessage(MsgTypeFullClientRequest, MsgTypeFlagNoSeq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg.Payload = payload
|
||||
frame, err := msg.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return conn.WriteMessage(websocket.BinaryMessage, frame)
|
||||
}
|
||||
305
relay/channel/volcengine/tts.go
Normal file
305
relay/channel/volcengine/tts.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package volcengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type VolcengineTTSRequest struct {
|
||||
App VolcengineTTSApp `json:"app"`
|
||||
User VolcengineTTSUser `json:"user"`
|
||||
Audio VolcengineTTSAudio `json:"audio"`
|
||||
Request VolcengineTTSReqInfo `json:"request"`
|
||||
}
|
||||
|
||||
type VolcengineTTSApp struct {
|
||||
AppID string `json:"appid"`
|
||||
Token string `json:"token"`
|
||||
Cluster string `json:"cluster"`
|
||||
}
|
||||
|
||||
type VolcengineTTSUser struct {
|
||||
UID string `json:"uid"`
|
||||
}
|
||||
|
||||
type VolcengineTTSAudio struct {
|
||||
VoiceType string `json:"voice_type"`
|
||||
Encoding string `json:"encoding"`
|
||||
SpeedRatio float64 `json:"speed_ratio"`
|
||||
Rate int `json:"rate"`
|
||||
Bitrate int `json:"bitrate,omitempty"`
|
||||
LoudnessRatio float64 `json:"loudness_ratio,omitempty"`
|
||||
EnableEmotion bool `json:"enable_emotion,omitempty"`
|
||||
Emotion string `json:"emotion,omitempty"`
|
||||
EmotionScale float64 `json:"emotion_scale,omitempty"`
|
||||
ExplicitLanguage string `json:"explicit_language,omitempty"`
|
||||
ContextLanguage string `json:"context_language,omitempty"`
|
||||
}
|
||||
|
||||
type VolcengineTTSReqInfo struct {
|
||||
ReqID string `json:"reqid"`
|
||||
Text string `json:"text"`
|
||||
Operation string `json:"operation"`
|
||||
Model string `json:"model,omitempty"`
|
||||
TextType string `json:"text_type,omitempty"`
|
||||
SilenceDuration float64 `json:"silence_duration,omitempty"`
|
||||
WithTimestamp interface{} `json:"with_timestamp,omitempty"`
|
||||
ExtraParam *VolcengineTTSExtraParam `json:"extra_param,omitempty"`
|
||||
}
|
||||
|
||||
type VolcengineTTSExtraParam struct {
|
||||
DisableMarkdownFilter bool `json:"disable_markdown_filter,omitempty"`
|
||||
EnableLatexTn bool `json:"enable_latex_tn,omitempty"`
|
||||
MuteCutThreshold string `json:"mute_cut_threshold,omitempty"`
|
||||
MuteCutRemainMs string `json:"mute_cut_remain_ms,omitempty"`
|
||||
DisableEmojiFilter bool `json:"disable_emoji_filter,omitempty"`
|
||||
UnsupportedCharRatioThresh float64 `json:"unsupported_char_ratio_thresh,omitempty"`
|
||||
AigcWatermark bool `json:"aigc_watermark,omitempty"`
|
||||
CacheConfig *VolcengineTTSCacheConfig `json:"cache_config,omitempty"`
|
||||
}
|
||||
|
||||
type VolcengineTTSCacheConfig struct {
|
||||
TextType int `json:"text_type,omitempty"`
|
||||
UseCache bool `json:"use_cache,omitempty"`
|
||||
}
|
||||
|
||||
type VolcengineTTSResponse struct {
|
||||
ReqID string `json:"reqid"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Sequence int `json:"sequence"`
|
||||
Data string `json:"data"`
|
||||
Addition *VolcengineTTSAdditionInfo `json:"addition,omitempty"`
|
||||
}
|
||||
|
||||
type VolcengineTTSAdditionInfo struct {
|
||||
Duration string `json:"duration"`
|
||||
}
|
||||
|
||||
var openAIToVolcengineVoiceMap = map[string]string{
|
||||
"alloy": "zh_male_M392_conversation_wvae_bigtts",
|
||||
"echo": "zh_male_wenhao_mars_bigtts",
|
||||
"fable": "zh_female_tianmei_mars_bigtts",
|
||||
"onyx": "zh_male_zhibei_mars_bigtts",
|
||||
"nova": "zh_female_shuangkuaisisi_mars_bigtts",
|
||||
"shimmer": "zh_female_cancan_mars_bigtts",
|
||||
}
|
||||
|
||||
var responseFormatToEncodingMap = map[string]string{
|
||||
"mp3": "mp3",
|
||||
"opus": "ogg_opus",
|
||||
"aac": "mp3",
|
||||
"flac": "mp3",
|
||||
"wav": "wav",
|
||||
"pcm": "pcm",
|
||||
}
|
||||
|
||||
func parseVolcengineAuth(apiKey string) (appID, token string, err error) {
|
||||
parts := strings.Split(apiKey, "|")
|
||||
if len(parts) != 2 {
|
||||
return "", "", errors.New("invalid api key format, expected: appid|access_token")
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
||||
func mapVoiceType(openAIVoice string) string {
|
||||
if voice, ok := openAIToVolcengineVoiceMap[openAIVoice]; ok {
|
||||
return voice
|
||||
}
|
||||
return openAIVoice
|
||||
}
|
||||
|
||||
func mapEncoding(responseFormat string) string {
|
||||
if encoding, ok := responseFormatToEncodingMap[responseFormat]; ok {
|
||||
return encoding
|
||||
}
|
||||
return "mp3"
|
||||
}
|
||||
|
||||
func getContentTypeByEncoding(encoding string) string {
|
||||
contentTypeMap := map[string]string{
|
||||
"mp3": "audio/mpeg",
|
||||
"ogg_opus": "audio/ogg",
|
||||
"wav": "audio/wav",
|
||||
"pcm": "audio/pcm",
|
||||
}
|
||||
if ct, ok := contentTypeMap[encoding]; ok {
|
||||
return ct
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
func handleTTSResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, encoding string) (usage any, err *types.NewAPIError) {
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
if readErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New("failed to read volcengine response"),
|
||||
types.ErrorCodeReadResponseBodyFailed,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var volcResp VolcengineTTSResponse
|
||||
if unmarshalErr := json.Unmarshal(body, &volcResp); unmarshalErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New("failed to parse volcengine response"),
|
||||
types.ErrorCodeBadResponseBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
if volcResp.Code != 3000 {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New(volcResp.Message),
|
||||
types.ErrorCodeBadResponse,
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
}
|
||||
|
||||
audioData, decodeErr := base64.StdEncoding.DecodeString(volcResp.Data)
|
||||
if decodeErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
errors.New("failed to decode audio data"),
|
||||
types.ErrorCodeBadResponseBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
contentType := getContentTypeByEncoding(encoding)
|
||||
c.Header("Content-Type", contentType)
|
||||
c.Data(http.StatusOK, contentType, audioData)
|
||||
|
||||
usage = &dto.Usage{
|
||||
PromptTokens: info.PromptTokens,
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: info.PromptTokens,
|
||||
}
|
||||
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
func generateRequestID() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
func handleTTSWebSocketResponse(c *gin.Context, requestURL string, volcRequest VolcengineTTSRequest, info *relaycommon.RelayInfo, encoding string) (usage any, err *types.NewAPIError) {
|
||||
_, token, parseErr := parseVolcengineAuth(info.ApiKey)
|
||||
if parseErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
parseErr,
|
||||
types.ErrorCodeChannelInvalidKey,
|
||||
http.StatusUnauthorized,
|
||||
)
|
||||
}
|
||||
|
||||
header := http.Header{}
|
||||
header.Set("Authorization", fmt.Sprintf("Bearer;%s", token))
|
||||
|
||||
conn, resp, dialErr := websocket.DefaultDialer.DialContext(context.Background(), requestURL, header)
|
||||
if dialErr != nil {
|
||||
if resp != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to connect to websocket: %w, status: %d", dialErr, resp.StatusCode),
|
||||
types.ErrorCodeBadResponseStatusCode,
|
||||
http.StatusBadGateway,
|
||||
)
|
||||
}
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to connect to websocket: %w", dialErr),
|
||||
types.ErrorCodeBadResponseStatusCode,
|
||||
http.StatusBadGateway,
|
||||
)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
payload, marshalErr := json.Marshal(volcRequest)
|
||||
if marshalErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to marshal request: %w", marshalErr),
|
||||
types.ErrorCodeBadRequestBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
if sendErr := FullClientRequest(conn, payload); sendErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to send request: %w", sendErr),
|
||||
types.ErrorCodeBadRequestBody,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
contentType := getContentTypeByEncoding(encoding)
|
||||
c.Header("Content-Type", contentType)
|
||||
c.Header("Transfer-Encoding", "chunked")
|
||||
|
||||
for {
|
||||
msg, recvErr := ReceiveMessage(conn)
|
||||
if recvErr != nil {
|
||||
if websocket.IsCloseError(recvErr, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
|
||||
break
|
||||
}
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to receive message: %w", recvErr),
|
||||
types.ErrorCodeBadResponse,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
|
||||
switch msg.MsgType {
|
||||
case MsgTypeError:
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("received error from server: code=%d, %s", msg.ErrorCode, string(msg.Payload)),
|
||||
types.ErrorCodeBadResponse,
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
case MsgTypeFrontEndResultServer:
|
||||
continue
|
||||
case MsgTypeAudioOnlyServer:
|
||||
if len(msg.Payload) > 0 {
|
||||
if _, writeErr := c.Writer.Write(msg.Payload); writeErr != nil {
|
||||
return nil, types.NewErrorWithStatusCode(
|
||||
fmt.Errorf("failed to write audio data: %w", writeErr),
|
||||
types.ErrorCodeBadResponse,
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
c.Writer.Flush()
|
||||
}
|
||||
|
||||
if msg.Sequence < 0 {
|
||||
c.Status(http.StatusOK)
|
||||
usage = &dto.Usage{
|
||||
PromptTokens: info.PromptTokens,
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: info.PromptTokens,
|
||||
}
|
||||
return usage, nil
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
usage = &dto.Usage{
|
||||
PromptTokens: info.PromptTokens,
|
||||
CompletionTokens: 0,
|
||||
TotalTokens: info.PromptTokens,
|
||||
}
|
||||
return usage, nil
|
||||
}
|
||||
@@ -263,6 +263,8 @@ var streamSupportedChannels = map[int]bool{
|
||||
constant.ChannelTypeDeepSeek: true,
|
||||
constant.ChannelTypeBaiduV2: true,
|
||||
constant.ChannelTypeZhipu_v4: true,
|
||||
constant.ChannelTypeAli: true,
|
||||
constant.ChannelTypeSubmodel: true,
|
||||
}
|
||||
|
||||
func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
@@ -22,9 +23,7 @@ func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types.
|
||||
// check auto group
|
||||
autoGroup, exists := ctx.Get("auto_group")
|
||||
if exists {
|
||||
if common.DebugEnabled {
|
||||
println(fmt.Sprintf("final group: %s", autoGroup))
|
||||
}
|
||||
logger.LogDebug(ctx, fmt.Sprintf("final group: %s", autoGroup))
|
||||
relayInfo.UsingGroup = autoGroup.(string)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
@@ -22,8 +23,10 @@ func GetAndValidateRequest(c *gin.Context, format types.RelayFormat) (request dt
|
||||
case types.RelayFormatOpenAI:
|
||||
request, err = GetAndValidateTextRequest(c, relayMode)
|
||||
case types.RelayFormatGemini:
|
||||
if strings.Contains(c.Request.URL.Path, ":embedContent") || strings.Contains(c.Request.URL.Path, ":batchEmbedContents") {
|
||||
if strings.Contains(c.Request.URL.Path, ":embedContent") {
|
||||
request, err = GetAndValidateGeminiEmbeddingRequest(c)
|
||||
} else if strings.Contains(c.Request.URL.Path, ":batchEmbedContents") {
|
||||
request, err = GetAndValidateGeminiBatchEmbeddingRequest(c)
|
||||
} else {
|
||||
request, err = GetAndValidateGeminiRequest(c)
|
||||
}
|
||||
@@ -60,19 +63,9 @@ func GetAndValidAudioRequest(c *gin.Context, relayMode int) (*dto.AudioRequest,
|
||||
return nil, errors.New("model is required")
|
||||
}
|
||||
default:
|
||||
err = c.Request.ParseForm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
formData := c.Request.PostForm
|
||||
if audioRequest.Model == "" {
|
||||
audioRequest.Model = formData.Get("model")
|
||||
}
|
||||
|
||||
if audioRequest.Model == "" {
|
||||
return nil, errors.New("model is required")
|
||||
}
|
||||
audioRequest.ResponseFormat = formData.Get("response_format")
|
||||
if audioRequest.ResponseFormat == "" {
|
||||
audioRequest.ResponseFormat = "json"
|
||||
}
|
||||
@@ -148,6 +141,9 @@ func GetAndValidOpenAIImageRequest(c *gin.Context, relayMode int) (*dto.ImageReq
|
||||
imageRequest.N = uint(common.String2Int(formData.Get("n")))
|
||||
imageRequest.Quality = formData.Get("quality")
|
||||
imageRequest.Size = formData.Get("size")
|
||||
if imageValue := formData.Get("image"); imageValue != "" {
|
||||
imageRequest.Image, _ = json.Marshal(imageValue)
|
||||
}
|
||||
|
||||
if imageRequest.Model == "gpt-image-1" {
|
||||
if imageRequest.Quality == "" {
|
||||
@@ -158,8 +154,9 @@ func GetAndValidOpenAIImageRequest(c *gin.Context, relayMode int) (*dto.ImageReq
|
||||
imageRequest.N = 1
|
||||
}
|
||||
|
||||
watermark := formData.Has("watermark")
|
||||
if watermark {
|
||||
hasWatermark := formData.Has("watermark")
|
||||
if hasWatermark {
|
||||
watermark := formData.Get("watermark") == "true"
|
||||
imageRequest.Watermark = &watermark
|
||||
}
|
||||
break
|
||||
@@ -319,3 +316,12 @@ func GetAndValidateGeminiEmbeddingRequest(c *gin.Context) (*dto.GeminiEmbeddingR
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func GetAndValidateGeminiBatchEmbeddingRequest(c *gin.Context) (*dto.GeminiBatchEmbeddingRequest, error) {
|
||||
request := &dto.GeminiBatchEmbeddingRequest{}
|
||||
err := common.UnmarshalBodyReusable(c, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/QuantumNous/new-api/relay/channel/gemini"
|
||||
"github.com/QuantumNous/new-api/relay/channel/jimeng"
|
||||
"github.com/QuantumNous/new-api/relay/channel/jina"
|
||||
"github.com/QuantumNous/new-api/relay/channel/minimax"
|
||||
"github.com/QuantumNous/new-api/relay/channel/mistral"
|
||||
"github.com/QuantumNous/new-api/relay/channel/mokaai"
|
||||
"github.com/QuantumNous/new-api/relay/channel/moonshot"
|
||||
@@ -108,6 +109,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
|
||||
return &moonshot.Adaptor{} // Moonshot uses Claude API
|
||||
case constant.APITypeSubmodel:
|
||||
return &submodel.Adaptor{}
|
||||
case constant.APITypeMiniMax:
|
||||
return &minimax.Adaptor{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -139,7 +142,7 @@ func GetTaskAdaptor(platform constant.TaskPlatform) channel.TaskAdaptor {
|
||||
return &taskVidu.TaskAdaptor{}
|
||||
case constant.ChannelTypeDoubaoVideo:
|
||||
return &taskdoubao.TaskAdaptor{}
|
||||
case constant.ChannelTypeSora:
|
||||
case constant.ChannelTypeSora, constant.ChannelTypeOpenAI:
|
||||
return &tasksora.TaskAdaptor{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +351,10 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d
|
||||
originTask.Progress = ti.Progress
|
||||
}
|
||||
if ti.Url != "" {
|
||||
originTask.FailReason = ti.Url
|
||||
if strings.HasPrefix(ti.Url, "data:") {
|
||||
} else {
|
||||
originTask.FailReason = ti.Url
|
||||
}
|
||||
}
|
||||
_ = originTask.Update()
|
||||
var raw map[string]any
|
||||
@@ -379,18 +382,20 @@ func videoFetchByIDRespBodyBuilder(c *gin.Context) (respBody []byte, taskResp *d
|
||||
case model.TaskStatusQueued, model.TaskStatusSubmitted:
|
||||
status = "queued"
|
||||
}
|
||||
out := map[string]any{
|
||||
"error": nil,
|
||||
"format": format,
|
||||
"metadata": nil,
|
||||
"status": status,
|
||||
"task_id": originTask.TaskID,
|
||||
"url": originTask.FailReason,
|
||||
if !strings.HasPrefix(c.Request.RequestURI, "/v1/videos/") {
|
||||
out := map[string]any{
|
||||
"error": nil,
|
||||
"format": format,
|
||||
"metadata": nil,
|
||||
"status": status,
|
||||
"task_id": originTask.TaskID,
|
||||
"url": originTask.FailReason,
|
||||
}
|
||||
respBody, _ = json.Marshal(dto.TaskResponse[any]{
|
||||
Code: "success",
|
||||
Data: out,
|
||||
})
|
||||
}
|
||||
respBody, _ = json.Marshal(dto.TaskResponse[any]{
|
||||
Code: "success",
|
||||
Data: out,
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter.GET("/ratio_config", middleware.CriticalRateLimit(), controller.GetRatioConfig)
|
||||
|
||||
apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
|
||||
apiRouter.POST("/creem/webhook", controller.CreemWebhook)
|
||||
|
||||
// Universal secure verification routes
|
||||
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify)
|
||||
@@ -81,6 +82,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
selfRoute.POST("/amount", controller.RequestAmount)
|
||||
selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay)
|
||||
selfRoute.POST("/stripe/amount", controller.RequestStripeAmount)
|
||||
selfRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.RequestCreemPay)
|
||||
selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
|
||||
selfRoute.PUT("/setting", controller.UpdateUserSetting)
|
||||
|
||||
|
||||
42
service/channel_select.go
Normal file
42
service/channel_select.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, modelName string, retry int) (*model.Channel, string, error) {
|
||||
var channel *model.Channel
|
||||
var err error
|
||||
selectGroup := group
|
||||
userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup)
|
||||
if group == "auto" {
|
||||
if len(setting.GetAutoGroups()) == 0 {
|
||||
return nil, selectGroup, errors.New("auto groups is not enabled")
|
||||
}
|
||||
for _, autoGroup := range GetUserAutoGroup(userGroup) {
|
||||
logger.LogDebug(c, "Auto selecting group:", autoGroup)
|
||||
channel, _ = model.GetRandomSatisfiedChannel(autoGroup, modelName, retry)
|
||||
if channel == nil {
|
||||
continue
|
||||
} else {
|
||||
c.Set("auto_group", autoGroup)
|
||||
selectGroup = autoGroup
|
||||
logger.LogDebug(c, "Auto selected group:", autoGroup)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel, err = model.GetRandomSatisfiedChannel(group, modelName, retry)
|
||||
if err != nil {
|
||||
return nil, group, err
|
||||
}
|
||||
}
|
||||
return channel, selectGroup, nil
|
||||
}
|
||||
54
service/group.go
Normal file
54
service/group.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
)
|
||||
|
||||
func GetUserUsableGroups(userGroup string) map[string]string {
|
||||
groupsCopy := setting.GetUserUsableGroupsCopy()
|
||||
if userGroup != "" {
|
||||
specialSettings, b := ratio_setting.GetGroupRatioSetting().GroupSpecialUsableGroup.Get(userGroup)
|
||||
if b {
|
||||
// 处理特殊可用分组
|
||||
for specialGroup, desc := range specialSettings {
|
||||
if strings.HasPrefix(specialGroup, "-:") {
|
||||
// 移除分组
|
||||
groupToRemove := strings.TrimPrefix(specialGroup, "-:")
|
||||
delete(groupsCopy, groupToRemove)
|
||||
} else if strings.HasPrefix(specialGroup, "+:") {
|
||||
// 添加分组
|
||||
groupToAdd := strings.TrimPrefix(specialGroup, "+:")
|
||||
groupsCopy[groupToAdd] = desc
|
||||
} else {
|
||||
// 直接添加分组
|
||||
groupsCopy[specialGroup] = desc
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果userGroup不在UserUsableGroups中,返回UserUsableGroups + userGroup
|
||||
if _, ok := groupsCopy[userGroup]; !ok {
|
||||
groupsCopy[userGroup] = "用户分组"
|
||||
}
|
||||
}
|
||||
return groupsCopy
|
||||
}
|
||||
|
||||
func GroupInUserUsableGroups(userGroup, groupName string) bool {
|
||||
_, ok := GetUserUsableGroups(userGroup)[groupName]
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetUserAutoGroup 根据用户分组获取自动分组设置
|
||||
func GetUserAutoGroup(userGroup string) []string {
|
||||
groups := GetUserUsableGroups(userGroup)
|
||||
autoGroups := make([]string, 0)
|
||||
for _, group := range setting.GetAutoGroups() {
|
||||
if _, ok := groups[group]; ok {
|
||||
autoGroups = append(autoGroups, group)
|
||||
}
|
||||
}
|
||||
return autoGroups
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
_ "image/png"
|
||||
"log"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
constant2 "github.com/QuantumNous/new-api/relay/constant"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -254,6 +256,10 @@ func getImageToken(fileMeta *types.FileMeta, model string, stream bool) (int, er
|
||||
}
|
||||
|
||||
func CountRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *relaycommon.RelayInfo) (int, error) {
|
||||
if meta == nil {
|
||||
return 0, errors.New("token count meta is nil")
|
||||
}
|
||||
|
||||
if !constant.GetMediaToken {
|
||||
return 0, nil
|
||||
}
|
||||
@@ -263,8 +269,29 @@ func CountRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *relayco
|
||||
if info.RelayFormat == types.RelayFormatOpenAIRealtime {
|
||||
return 0, nil
|
||||
}
|
||||
if meta == nil {
|
||||
return 0, errors.New("token count meta is nil")
|
||||
if info.RelayMode == constant2.RelayModeAudioTranscription || info.RelayMode == constant2.RelayModeAudioTranslation {
|
||||
multiForm, err := common.ParseMultipartFormReusable(c)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error parsing multipart form: %v", err)
|
||||
}
|
||||
fileHeaders := multiForm.File["file"]
|
||||
totalAudioToken := 0
|
||||
for _, fileHeader := range fileHeaders {
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error opening audio file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
// get ext and io.seeker
|
||||
ext := filepath.Ext(fileHeader.Filename)
|
||||
duration, err := common.GetAudioDuration(c.Request.Context(), file, ext)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error getting audio duration: %v", err)
|
||||
}
|
||||
// 一分钟 1000 token,与 $price / minute 对齐
|
||||
totalAudioToken += int(math.Round(math.Ceil(duration) / 60.0 * 1000))
|
||||
}
|
||||
return totalAudioToken, nil
|
||||
}
|
||||
|
||||
model := common.GetContextKeyString(c, constant.ContextKeyOriginalModel)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package setting
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
)
|
||||
|
||||
var AutoGroups = []string{
|
||||
var autoGroups = []string{
|
||||
"default",
|
||||
}
|
||||
|
||||
var DefaultUseAutoGroup = false
|
||||
|
||||
func ContainsAutoGroup(group string) bool {
|
||||
for _, autoGroup := range AutoGroups {
|
||||
for _, autoGroup := range autoGroups {
|
||||
if autoGroup == group {
|
||||
return true
|
||||
}
|
||||
@@ -18,14 +20,18 @@ func ContainsAutoGroup(group string) bool {
|
||||
}
|
||||
|
||||
func UpdateAutoGroupsByJsonString(jsonString string) error {
|
||||
AutoGroups = make([]string, 0)
|
||||
return json.Unmarshal([]byte(jsonString), &AutoGroups)
|
||||
autoGroups = make([]string, 0)
|
||||
return common.Unmarshal([]byte(jsonString), &autoGroups)
|
||||
}
|
||||
|
||||
func AutoGroups2JsonString() string {
|
||||
jsonBytes, err := json.Marshal(AutoGroups)
|
||||
jsonBytes, err := common.Marshal(autoGroups)
|
||||
if err != nil {
|
||||
return "[]"
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
func GetAutoGroups() []string {
|
||||
return autoGroups
|
||||
}
|
||||
|
||||
@@ -131,6 +131,18 @@ func configToMap(config interface{}) (map[string]string, error) {
|
||||
strValue = strconv.FormatUint(field.Uint(), 10)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
strValue = strconv.FormatFloat(field.Float(), 'f', -1, 64)
|
||||
case reflect.Ptr:
|
||||
// 处理指针类型:如果非 nil,序列化指向的值
|
||||
if !field.IsNil() {
|
||||
bytes, err := json.Marshal(field.Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
strValue = string(bytes)
|
||||
} else {
|
||||
// nil 指针序列化为 "null"
|
||||
strValue = "null"
|
||||
}
|
||||
case reflect.Map, reflect.Slice, reflect.Struct:
|
||||
// 复杂类型使用JSON序列化
|
||||
bytes, err := json.Marshal(field.Interface())
|
||||
@@ -215,6 +227,21 @@ func updateConfigFromMap(config interface{}, configMap map[string]string) error
|
||||
continue
|
||||
}
|
||||
field.SetFloat(floatValue)
|
||||
case reflect.Ptr:
|
||||
// 处理指针类型
|
||||
if strValue == "null" {
|
||||
field.Set(reflect.Zero(field.Type()))
|
||||
} else {
|
||||
// 如果指针是 nil,需要先初始化
|
||||
if field.IsNil() {
|
||||
field.Set(reflect.New(field.Type().Elem()))
|
||||
}
|
||||
// 反序列化到指针指向的值
|
||||
err := json.Unmarshal([]byte(strValue), field.Interface())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
case reflect.Map, reflect.Slice, reflect.Struct:
|
||||
// 复杂类型使用JSON反序列化
|
||||
err := json.Unmarshal([]byte(strValue), field.Addr().Interface())
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
)
|
||||
|
||||
type MonitorSetting struct {
|
||||
AutoTestChannelEnabled bool `json:"auto_test_channel_enabled"`
|
||||
AutoTestChannelMinutes int `json:"auto_test_channel_minutes"`
|
||||
AutoTestChannelEnabled bool `json:"auto_test_channel_enabled"`
|
||||
AutoTestChannelMinutes float64 `json:"auto_test_channel_minutes"`
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
@@ -28,7 +28,7 @@ func GetMonitorSetting() *MonitorSetting {
|
||||
frequency, err := strconv.Atoi(os.Getenv("CHANNEL_TEST_FREQUENCY"))
|
||||
if err == nil && frequency > 0 {
|
||||
monitorSetting.AutoTestChannelEnabled = true
|
||||
monitorSetting.AutoTestChannelMinutes = frequency
|
||||
monitorSetting.AutoTestChannelMinutes = float64(frequency)
|
||||
}
|
||||
}
|
||||
return &monitorSetting
|
||||
|
||||
6
setting/payment_creem.go
Normal file
6
setting/payment_creem.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package setting
|
||||
|
||||
var CreemApiKey = ""
|
||||
var CreemProducts = "[]"
|
||||
var CreemTestMode = false
|
||||
var CreemWebhookSecret = ""
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/setting/config"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
)
|
||||
|
||||
var groupRatio = map[string]float64{
|
||||
@@ -13,6 +15,7 @@ var groupRatio = map[string]float64{
|
||||
"vip": 1,
|
||||
"svip": 1,
|
||||
}
|
||||
|
||||
var groupRatioMutex sync.RWMutex
|
||||
|
||||
var (
|
||||
@@ -24,6 +27,42 @@ var (
|
||||
groupGroupRatioMutex sync.RWMutex
|
||||
)
|
||||
|
||||
var defaultGroupSpecialUsableGroup = map[string]map[string]string{
|
||||
"vip": {
|
||||
"append_1": "vip_special_group_1",
|
||||
"-:remove_1": "vip_removed_group_1",
|
||||
},
|
||||
}
|
||||
|
||||
type GroupRatioSetting struct {
|
||||
GroupRatio map[string]float64 `json:"group_ratio"`
|
||||
GroupGroupRatio map[string]map[string]float64 `json:"group_group_ratio"`
|
||||
GroupSpecialUsableGroup *types.RWMap[string, map[string]string] `json:"group_special_usable_group"`
|
||||
}
|
||||
|
||||
var groupRatioSetting GroupRatioSetting
|
||||
|
||||
func init() {
|
||||
groupSpecialUsableGroup := types.NewRWMap[string, map[string]string]()
|
||||
groupSpecialUsableGroup.AddAll(defaultGroupSpecialUsableGroup)
|
||||
|
||||
groupRatioSetting = GroupRatioSetting{
|
||||
GroupSpecialUsableGroup: groupSpecialUsableGroup,
|
||||
GroupRatio: groupRatio,
|
||||
GroupGroupRatio: GroupGroupRatio,
|
||||
}
|
||||
|
||||
config.GlobalConfig.Register("group_ratio_setting", &groupRatioSetting)
|
||||
}
|
||||
|
||||
func GetGroupRatioSetting() *GroupRatioSetting {
|
||||
if groupRatioSetting.GroupSpecialUsableGroup == nil {
|
||||
groupRatioSetting.GroupSpecialUsableGroup = types.NewRWMap[string, map[string]string]()
|
||||
groupRatioSetting.GroupSpecialUsableGroup.AddAll(defaultGroupSpecialUsableGroup)
|
||||
}
|
||||
return &groupRatioSetting
|
||||
}
|
||||
|
||||
func GetGroupRatioCopy() map[string]float64 {
|
||||
groupRatioMutex.RLock()
|
||||
defer groupRatioMutex.RUnlock()
|
||||
|
||||
@@ -43,29 +43,6 @@ func UpdateUserUsableGroupsByJSONString(jsonStr string) error {
|
||||
return json.Unmarshal([]byte(jsonStr), &userUsableGroups)
|
||||
}
|
||||
|
||||
func GetUserUsableGroups(userGroup string) map[string]string {
|
||||
groupsCopy := GetUserUsableGroupsCopy()
|
||||
if userGroup == "" {
|
||||
if _, ok := groupsCopy["default"]; !ok {
|
||||
groupsCopy["default"] = "default"
|
||||
}
|
||||
}
|
||||
// 如果userGroup不在UserUsableGroups中,返回UserUsableGroups + userGroup
|
||||
if _, ok := groupsCopy[userGroup]; !ok {
|
||||
groupsCopy[userGroup] = "用户分组"
|
||||
}
|
||||
// 如果userGroup在UserUsableGroups中,返回UserUsableGroups
|
||||
return groupsCopy
|
||||
}
|
||||
|
||||
func GroupInUserUsableGroups(groupName string) bool {
|
||||
userUsableGroupsMutex.RLock()
|
||||
defer userUsableGroupsMutex.RUnlock()
|
||||
|
||||
_, ok := userUsableGroups[groupName]
|
||||
return ok
|
||||
}
|
||||
|
||||
func GetUsableGroupDescription(groupName string) string {
|
||||
userUsableGroupsMutex.RLock()
|
||||
defer userUsableGroupsMutex.RUnlock()
|
||||
|
||||
82
types/rw_map.go
Normal file
82
types/rw_map.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
)
|
||||
|
||||
type RWMap[K comparable, V any] struct {
|
||||
data map[K]V
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) UnmarshalJSON(b []byte) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.data = make(map[K]V)
|
||||
return common.Unmarshal(b, &m.data)
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) MarshalJSON() ([]byte, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return common.Marshal(m.data)
|
||||
}
|
||||
|
||||
func NewRWMap[K comparable, V any]() *RWMap[K, V] {
|
||||
return &RWMap[K, V]{
|
||||
data: make(map[K]V),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) Get(key K) (V, bool) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
value, exists := m.data[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) Set(key K, value V) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.data[key] = value
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) AddAll(other map[K]V) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
for k, v := range other {
|
||||
m.data[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) Clear() {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.data = make(map[K]V)
|
||||
}
|
||||
|
||||
// ReadAll returns a copy of the entire map.
|
||||
func (m *RWMap[K, V]) ReadAll() map[K]V {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
copiedMap := make(map[K]V)
|
||||
for k, v := range m.data {
|
||||
copiedMap[k] = v
|
||||
}
|
||||
return copiedMap
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) Len() int {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return len(m.data)
|
||||
}
|
||||
|
||||
func LoadFromJsonString[K comparable, V any](m *RWMap[K, V], jsonStr string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.data = make(map[K]V)
|
||||
return common.Unmarshal([]byte(jsonStr), &m.data)
|
||||
}
|
||||
223
web/bun.lock
223
web/bun.lock
@@ -50,6 +50,7 @@
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"i18next-cli": "^1.10.3",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.0.0",
|
||||
"tailwindcss": "^3",
|
||||
@@ -271,6 +272,42 @@
|
||||
|
||||
"@iconify/utils": ["@iconify/utils@2.3.0", "", { "dependencies": { "@antfu/install-pkg": "^1.0.0", "@antfu/utils": "^8.1.0", "@iconify/types": "^2.0.0", "debug": "^4.4.0", "globals": "^15.14.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "mlly": "^1.7.4" } }, "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA=="],
|
||||
|
||||
"@inquirer/ansi": ["@inquirer/ansi@1.0.1", "", {}, "sha512-yqq0aJW/5XPhi5xOAL1xRCpe1eh8UFVgYFpFsjEqmIR8rKLyP+HINvFXwUaxYICflJrVlxnp7lLN6As735kVpw=="],
|
||||
|
||||
"@inquirer/checkbox": ["@inquirer/checkbox@4.3.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/figures": "^1.0.14", "@inquirer/type": "^3.0.9", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5+Q3PKH35YsnoPTh75LucALdAxom6xh5D1oeY561x4cqBuH24ZFVyFREPe14xgnrtmGu3EEt1dIi60wRVSnGCw=="],
|
||||
|
||||
"@inquirer/confirm": ["@inquirer/confirm@5.1.19", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wQNz9cfcxrtEnUyG5PndC8g3gZ7lGDBzmWiXZkX8ot3vfZ+/BLjR8EvyGX4YzQLeVqtAlY/YScZpW7CW8qMoDQ=="],
|
||||
|
||||
"@inquirer/core": ["@inquirer/core@10.3.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/figures": "^1.0.14", "@inquirer/type": "^3.0.9", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Uv2aPPPSK5jeCplQmQ9xadnFx2Zhj9b5Dj7bU6ZeCdDNNY11nhYy4btcSdtDguHqCT2h5oNeQTcUNSGGLA7NTA=="],
|
||||
|
||||
"@inquirer/editor": ["@inquirer/editor@4.2.21", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/external-editor": "^1.0.2", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-MjtjOGjr0Kh4BciaFShYpZ1s9400idOdvQ5D7u7lE6VztPFoyLcVNE5dXBmEEIQq5zi4B9h2kU+q7AVBxJMAkQ=="],
|
||||
|
||||
"@inquirer/expand": ["@inquirer/expand@4.0.21", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+mScLhIcbPFmuvU3tAGBed78XvYHSvCl6dBiYMlzCLhpr0bzGzd8tfivMMeqND6XZiaZ1tgusbUHJEfc6YzOdA=="],
|
||||
|
||||
"@inquirer/external-editor": ["@inquirer/external-editor@1.0.2", "", { "dependencies": { "chardet": "^2.1.0", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yy9cOoBnx58TlsPrIxauKIFQTiyH+0MK4e97y4sV9ERbI+zDxw7i2hxHLCIEGIE/8PPvDxGhgzIOTSOWcs6/MQ=="],
|
||||
|
||||
"@inquirer/figures": ["@inquirer/figures@1.0.14", "", {}, "sha512-DbFgdt+9/OZYFM+19dbpXOSeAstPy884FPy1KjDu4anWwymZeOYhMY1mdFri172htv6mvc/uvIAAi7b7tvjJBQ=="],
|
||||
|
||||
"@inquirer/input": ["@inquirer/input@4.2.5", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-7GoWev7P6s7t0oJbenH0eQ0ThNdDJbEAEtVt9vsrYZ9FulIokvd823yLyhQlWHJPGce1wzP53ttfdCZmonMHyA=="],
|
||||
|
||||
"@inquirer/number": ["@inquirer/number@3.0.21", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5QWs0KGaNMlhbdhOSCFfKsW+/dcAVC2g4wT/z2MCiZM47uLgatC5N20kpkDQf7dHx+XFct/MJvvNGy6aYJn4Pw=="],
|
||||
|
||||
"@inquirer/password": ["@inquirer/password@4.0.21", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-xxeW1V5SbNFNig2pLfetsDb0svWlKuhmr7MPJZMYuDnCTkpVBI+X/doudg4pznc1/U+yYmWFFOi4hNvGgUo7EA=="],
|
||||
|
||||
"@inquirer/prompts": ["@inquirer/prompts@7.9.0", "", { "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", "@inquirer/editor": "^4.2.21", "@inquirer/expand": "^4.0.21", "@inquirer/input": "^4.2.5", "@inquirer/number": "^3.0.21", "@inquirer/password": "^4.0.21", "@inquirer/rawlist": "^4.1.9", "@inquirer/search": "^3.2.0", "@inquirer/select": "^4.4.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A=="],
|
||||
|
||||
"@inquirer/rawlist": ["@inquirer/rawlist@4.1.9", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/type": "^3.0.9", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-AWpxB7MuJrRiSfTKGJ7Y68imYt8P9N3Gaa7ySdkFj1iWjr6WfbGAhdZvw/UnhFXTHITJzxGUI9k8IX7akAEBCg=="],
|
||||
|
||||
"@inquirer/search": ["@inquirer/search@3.2.0", "", { "dependencies": { "@inquirer/core": "^10.3.0", "@inquirer/figures": "^1.0.14", "@inquirer/type": "^3.0.9", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-a5SzB/qrXafDX1Z4AZW3CsVoiNxcIYCzYP7r9RzrfMpaLpB+yWi5U8BWagZyLmwR0pKbbL5umnGRd0RzGVI8bQ=="],
|
||||
|
||||
"@inquirer/select": ["@inquirer/select@4.4.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/figures": "^1.0.14", "@inquirer/type": "^3.0.9", "yoctocolors-cjs": "^2.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kaC3FHsJZvVyIjYBs5Ih8y8Bj4P/QItQWrZW22WJax7zTN+ZPXVGuOM55vzbdCP9zKUiBd9iEJVdesujfF+cAA=="],
|
||||
|
||||
"@inquirer/type": ["@inquirer/type@3.0.9", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QPaNt/nmE2bLGQa9b7wwyRJoLZ7pN6rcyXvzU0YCmivmJyq1BVo94G98tStRWkoD1RgDX5C+dPlhhHzNdu/W/w=="],
|
||||
|
||||
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
||||
|
||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
@@ -461,6 +498,8 @@
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.30.0", "", { "os": "win32", "cpu": "x64" }, "sha512-DYvxS0M07PvgvavMIybCOBYheyrqlui6ZQBHJs6GqduVzHSZ06TPPvlfvnYstjODHQ8UUXFwt5YE+h0jFI8kwg=="],
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-1/adJbSMBOkpScCE/SB6XkjJU17ANln3Wky7lOmrnpl+zBdQ1qXUJg2GXTYVHRq+2j3hd1DesmElTXYDgtfSOQ=="],
|
||||
@@ -477,6 +516,8 @@
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
"@so1ve/prettier-config": ["@so1ve/prettier-config@3.1.0", "", { "dependencies": { "@so1ve/prettier-plugin-toml": "3.1.0", "prettier-plugin-astro": "^0.14.0", "prettier-plugin-curly-and-jsdoc": "3.1.0", "prettier-plugin-pkgsort": "^0.2.1" }, "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-9GJ1yXKBC4DzqCTTaZoBf8zw7WWkVuXcccZt1Aqk4lj6ab/GiNUnjPGajUVYLjaqAEOKqM7jUSUfTjk2JTjCAg=="],
|
||||
|
||||
"@so1ve/prettier-plugin-toml": ["@so1ve/prettier-plugin-toml@3.1.0", "", { "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-8WZAGjAVNIJlkfWL6wHKxlUuEBY45fdd5qY5bR/Z6r/txgzKXk/r9qi1DTwc17gi/WcNuRrcRugecRT+mWbIYg=="],
|
||||
@@ -485,6 +526,32 @@
|
||||
|
||||
"@stitches/react": ["@stitches/react@1.2.8", "", { "peerDependencies": { "react": ">= 16.3.0" } }, "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA=="],
|
||||
|
||||
"@swc/core": ["@swc/core@1.13.19", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.13.19", "@swc/core-darwin-x64": "1.13.19", "@swc/core-linux-arm-gnueabihf": "1.13.19", "@swc/core-linux-arm64-gnu": "1.13.19", "@swc/core-linux-arm64-musl": "1.13.19", "@swc/core-linux-x64-gnu": "1.13.19", "@swc/core-linux-x64-musl": "1.13.19", "@swc/core-win32-arm64-msvc": "1.13.19", "@swc/core-win32-ia32-msvc": "1.13.19", "@swc/core-win32-x64-msvc": "1.13.19" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-V1r4wFdjaZIUIZZrV2Mb/prEeu03xvSm6oatPxsvnXKF9lNh5Jtk9QvUdiVfD9rrvi7bXrAVhg9Wpbmv/2Fl1g=="],
|
||||
|
||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.13.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NxDyte9tCJSJ8+R62WDtqwg8eI57lubD52sHyGOfezpJBOPr36bUSGGLyO3Vod9zTGlOu2CpkuzA/2iVw92u1g=="],
|
||||
|
||||
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.13.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-+w5DYrJndSygFFRDcuPYmx5BljD6oYnAohZ15K1L6SfORHp/BTSIbgSFRKPoyhjuIkDiq3W0um8RoMTOBAcQjQ=="],
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.13.19", "", { "os": "linux", "cpu": "arm" }, "sha512-7LlfgpdwwYq2q7himNkAAFo4q6jysMLFNoBH6GRP7WL29NcSsl5mPMJjmYZymK+sYq/9MTVieDTQvChzYDsapw=="],
|
||||
|
||||
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.13.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-ml3I6Lm2marAQ3UC/TS9t/yILBh/eDSVHAdPpikp652xouWAVW1znUeV6bBSxe1sSZIenv+p55ubKAWq/u84sQ=="],
|
||||
|
||||
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.13.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-M/otFc3/rWWkbF6VgbOXVzUKVoE7MFcphTaStxJp4bwb7oP5slYlxMZN51Dk/OTOfvCDo9pTAFDKNyixbkXMDQ=="],
|
||||
|
||||
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.13.19", "", { "os": "linux", "cpu": "x64" }, "sha512-NoMUKaOJEdouU4tKF88ggdDHFiRRING+gYLxDqnTfm+sUXaizB5OGBRzvSVDYSXQb1SuUuChnXFPFzwTWbt3ZQ=="],
|
||||
|
||||
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.13.19", "", { "os": "linux", "cpu": "x64" }, "sha512-r6krlZwyu8SBaw24QuS1lau2I9q8M+eJV6ITz0rpb6P1Bx0elf9ii5Bhh8ddmIqXXH8kOGSjC/dwcdHbZqAhgw=="],
|
||||
|
||||
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.13.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-awcZSIuxyVn0Dw28VjMvgk1qiDJ6CeQwHkZNUjg2UxVlq23zE01NMMp+zkoGFypmLG9gaGmJSzuoqvk/WCQ5tw=="],
|
||||
|
||||
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.13.19", "", { "os": "win32", "cpu": "ia32" }, "sha512-H5d+KO7ISoLNgYvTbOcCQjJZNM3R7yaYlrMAF13lUr6GSiOUX+92xtM31B+HvzAWI7HtvVe74d29aC1b1TpXFA=="],
|
||||
|
||||
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.13.19", "", { "os": "win32", "cpu": "x64" }, "sha512-qNoyCpXvv2O3JqXKanRIeoMn03Fho/As+N4Fhe7u0FsYh4VYqGQah4DGDzEP/yjl4Gx1IElhqLGDhCCGMwWaDw=="],
|
||||
|
||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||
|
||||
"@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="],
|
||||
|
||||
"@turf/boolean-clockwise": ["@turf/boolean-clockwise@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-45+C7LC5RMbRWrxh3Z0Eihsc8db1VGBO5d9BLTOAwU4jR6SgsunTfRWR16X7JUwIDYlCVEmnjcXJNi/kIU3VIw=="],
|
||||
|
||||
"@turf/clone": ["@turf/clone@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0" } }, "sha512-mzVtTFj/QycXOn6ig+annKrM6ZlimreKYz6f/GSERytOpgzodbQyOgkfwru100O1KQhhjSudKK4DsQ0oyi9cTw=="],
|
||||
@@ -651,6 +718,8 @@
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
|
||||
|
||||
"ahooks": ["ahooks@3.8.5", "", { "dependencies": { "@babel/runtime": "^7.21.0", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Y+MLoJpBXVdjsnnBjE5rOSPkQ4DK+8i5aPDzLJdIOsCpo/fiAeXcBY1Y7oWgtOK0TpOz0gFa/XcyO1UGdoqLcw=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
@@ -733,11 +802,13 @@
|
||||
|
||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||
|
||||
"chardet": ["chardet@2.1.0", "", {}, "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA=="],
|
||||
|
||||
"chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="],
|
||||
|
||||
"chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
|
||||
|
||||
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"chroma-js": ["chroma-js@3.1.2", "", {}, "sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg=="],
|
||||
|
||||
@@ -745,6 +816,12 @@
|
||||
|
||||
"classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||
|
||||
"cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="],
|
||||
|
||||
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
|
||||
@@ -759,7 +836,7 @@
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
"commander": ["commander@14.0.1", "", {}, "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A=="],
|
||||
|
||||
"compute-scroll-into-view": ["compute-scroll-into-view@1.0.20", "", {}, "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="],
|
||||
|
||||
@@ -965,6 +1042,8 @@
|
||||
|
||||
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||
|
||||
"execa": ["execa@9.6.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw=="],
|
||||
|
||||
"exsolve": ["exsolve@1.0.5", "", {}, "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
@@ -983,6 +1062,8 @@
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
|
||||
|
||||
"file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="],
|
||||
@@ -1031,17 +1112,21 @@
|
||||
|
||||
"geojson-linestring-dissolve": ["geojson-linestring-dissolve@0.0.1", "", {}, "sha512-Y8I2/Ea28R/Xeki7msBcpMvJL2TaPfaPKP8xqueJfQ9/jEhps+iOJxOR2XCBGgVb12Z6XnDb1CMbaPfLepsLaw=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-stdin": ["get-stdin@6.0.0", "", {}, "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g=="],
|
||||
|
||||
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
||||
|
||||
"get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="],
|
||||
|
||||
"giscus": ["giscus@1.6.0", "", { "dependencies": { "lit": "^3.2.1" } }, "sha512-Zrsi8r4t1LVW950keaWcsURuZUQwUaMKjvJgTCY125vkW6OiEBkatE7ScJDbpqKHdZwb///7FVC21SE3iFK3PQ=="],
|
||||
|
||||
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
"glob": ["glob@11.0.3", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
@@ -1103,10 +1188,16 @@
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||
|
||||
"i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="],
|
||||
|
||||
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@7.2.2", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ=="],
|
||||
|
||||
"i18next-cli": ["i18next-cli@1.15.0", "", { "dependencies": { "@swc/core": "1.13.19", "chalk": "5.6.2", "chokidar": "4.0.3", "commander": "14.0.1", "execa": "9.6.0", "glob": "11.0.3", "i18next-resources-for-ts": "1.7.4", "inquirer": "12.9.6", "jiti": "2.6.1", "jsonc-parser": "3.3.1", "minimatch": "10.0.3", "ora": "9.0.0", "swc-walk": "1.0.0" }, "bin": { "i18next-cli": "dist/esm/cli.js" } }, "sha512-XgIvoSTInr0uvf8lun5t9ZDT3wa6aj/annRIYBF0WcpERowLKrRQNBLDeDNkvpddInhNlRtUOhP24F0oOLZY+Q=="],
|
||||
|
||||
"i18next-resources-for-ts": ["i18next-resources-for-ts@1.7.4", "", { "dependencies": { "@babel/runtime": "^7.27.0", "yaml": "^2.7.1" }, "bin": { "i18next-resources-for-ts": "bin/i18next-resources-for-ts.js" } }, "sha512-3NpN2zasOWYR5zWA4JIdFhxrHxRJV8HEsbR7/GHSnotfjArjZzKvOzQnLFZ911QFmmcwq80saw8rccpHH+MYVQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
@@ -1127,6 +1218,8 @@
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
|
||||
|
||||
"inquirer": ["inquirer@12.9.6", "", { "dependencies": { "@inquirer/ansi": "^1.0.0", "@inquirer/core": "^10.2.2", "@inquirer/prompts": "^7.8.6", "@inquirer/type": "^3.0.8", "mute-stream": "^2.0.0", "run-async": "^4.0.5", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-603xXOgyfxhuis4nfnWaZrMaotNT0Km9XwwBNWUKbIDqeCY89jGr2F9YPEMiNhU6XjIP4VoWISMBFfcc5NgrTw=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"intersection-observer": ["intersection-observer@0.12.2", "", {}, "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="],
|
||||
@@ -1153,6 +1246,8 @@
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
|
||||
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
|
||||
@@ -1161,15 +1256,19 @@
|
||||
|
||||
"is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
|
||||
|
||||
"is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="],
|
||||
|
||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
"jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="],
|
||||
|
||||
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
|
||||
|
||||
@@ -1233,6 +1332,8 @@
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
@@ -1375,6 +1476,8 @@
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.6", "", {}, "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="],
|
||||
@@ -1391,6 +1494,8 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
|
||||
@@ -1405,6 +1510,8 @@
|
||||
|
||||
"normalize-range": ["normalize-range@0.1.2", "", {}, "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
|
||||
|
||||
"numeral": ["numeral@2.0.6", "", {}, "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
@@ -1415,12 +1522,16 @@
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
||||
|
||||
"oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"ora": ["ora@9.0.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", "string-width": "^8.1.0", "strip-ansi": "^7.1.2" } }, "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
@@ -1437,6 +1548,8 @@
|
||||
|
||||
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
|
||||
|
||||
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
||||
|
||||
"parse-svg-path": ["parse-svg-path@0.1.2", "", {}, "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ=="],
|
||||
|
||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
@@ -1453,7 +1566,7 @@
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
"path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="],
|
||||
|
||||
"path-source": ["path-source@0.1.3", "", { "dependencies": { "array-source": "0.0", "file-source": "0.6" } }, "sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw=="],
|
||||
|
||||
@@ -1507,6 +1620,8 @@
|
||||
|
||||
"prettier-plugin-pkgsort": ["prettier-plugin-pkgsort@0.2.1", "", { "dependencies": { "prettier-package-json": "^2.8.0" }, "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-/k5MIw84EhgoH7dmq4+6ozHjJ0VYbxbw17g4C+WPGHODkLivGwJoA6U1YPR/KObyRDMQJHXAfXKu++9smg7Jyw=="],
|
||||
|
||||
"pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
|
||||
|
||||
"prismjs": ["prismjs@1.29.0", "", {}, "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
@@ -1531,7 +1646,7 @@
|
||||
|
||||
"rc-checkbox": ["rc-checkbox@3.5.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.25.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg=="],
|
||||
|
||||
"rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="],
|
||||
"rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="],
|
||||
|
||||
"rc-dialog": ["rc-dialog@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="],
|
||||
|
||||
@@ -1655,7 +1770,7 @@
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
|
||||
|
||||
@@ -1703,6 +1818,8 @@
|
||||
|
||||
"resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||
@@ -1713,10 +1830,14 @@
|
||||
|
||||
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
|
||||
|
||||
"run-async": ["run-async@4.0.6", "", {}, "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
||||
|
||||
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||
|
||||
"s.color": ["s.color@0.0.15", "", {}, "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
@@ -1773,11 +1894,13 @@
|
||||
|
||||
"sse.js": ["sse.js@2.6.0", "", {}, "sha512-eGEqOwiPX9Cm+KsOYkcz7HIEqWUSOFeChr0sT515hDOBLvQy5yxaLSZx9JWMhwjf75CXJq+7cgG1MKNh9GQ36w=="],
|
||||
|
||||
"stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
|
||||
|
||||
"stream-source": ["stream-source@0.3.5", "", {}, "sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g=="],
|
||||
|
||||
"string-convert": ["string-convert@0.2.1", "", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="],
|
||||
|
||||
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
"string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
@@ -1789,6 +1912,8 @@
|
||||
|
||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
|
||||
@@ -1803,6 +1928,8 @@
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"swc-walk": ["swc-walk@1.0.0", "", { "dependencies": { "acorn-walk": "^8.3.4" } }, "sha512-QnEvBZ/ZRsUrXCz/Z3Kto06xUsoqUTo3doj/UvOD0RfamEgqlhpgpyCykFAwiUcuDrODShzlxuDqDPf2Wc+DvQ=="],
|
||||
|
||||
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
|
||||
|
||||
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
|
||||
@@ -1851,6 +1978,8 @@
|
||||
|
||||
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
|
||||
|
||||
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
|
||||
@@ -1921,7 +2050,7 @@
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
@@ -1933,6 +2062,10 @@
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||
|
||||
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
|
||||
|
||||
"zustand": ["zustand@3.7.2", "", { "peerDependencies": { "react": ">=16.8" }, "optionalPeers": ["react"] }, "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
@@ -1965,8 +2098,14 @@
|
||||
|
||||
"@iconify/utils/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="],
|
||||
|
||||
"@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
|
||||
|
||||
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"@lobehub/fluent-emoji/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="],
|
||||
|
||||
"@lobehub/icons/lucide-react": ["lucide-react@0.469.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw=="],
|
||||
@@ -1975,8 +2114,6 @@
|
||||
|
||||
"@lobehub/ui/lucide-react": ["lucide-react@0.484.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-oZy8coK9kZzvqhSgfbGkPtTgyjpBvs3ukLgDPv14dSOZtBtboryWF5o8i3qen7QbGg7JhiJBz5mK1p8YoMZTLQ=="],
|
||||
|
||||
"@lobehub/ui/rc-collapse": ["rc-collapse@4.0.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-SwoOByE39/3oIokDs/BnkqI+ltwirZbP8HZdq1/3SkPSBi7xDdvWHTp7cpNI9ullozkR6mwTWQi6/E/9huQVrA=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
|
||||
|
||||
"@radix-ui/react-popper/@floating-ui/react-dom": ["@floating-ui/react-dom@0.7.2", "", { "dependencies": { "@floating-ui/dom": "^0.5.3", "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg=="],
|
||||
@@ -1993,9 +2130,9 @@
|
||||
|
||||
"@visactor/vrender-kits/roughjs": ["roughjs@4.5.2", "", { "dependencies": { "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-2xSlLDKdsWyFxrveYWk9YQ/Y9UfK38EAMRNkYkMqYBJvPX8abCa9PN0x3w02H8Oa6/0bcZICJU+U95VumPqseg=="],
|
||||
|
||||
"antd/scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
|
||||
"antd/rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="],
|
||||
|
||||
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
"antd/scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
|
||||
|
||||
"cosmiconfig/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||
|
||||
@@ -2025,7 +2162,7 @@
|
||||
|
||||
"geojson-flatten/minimist": ["minimist@1.2.0", "", {}, "sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
"glob/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
||||
|
||||
"hast-util-from-parse5/property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
|
||||
|
||||
@@ -2035,6 +2172,14 @@
|
||||
|
||||
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"i18next-cli/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"i18next-cli/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="],
|
||||
|
||||
"i18next-resources-for-ts/@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"leva/react-dropzone": ["react-dropzone@12.1.0", "", { "dependencies": { "attr-accept": "^2.2.2", "file-selector": "^0.5.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.8" } }, "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog=="],
|
||||
|
||||
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
@@ -2047,9 +2192,15 @@
|
||||
|
||||
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"ora/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
"path-scurry/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
|
||||
|
||||
"prettier-package-json/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
|
||||
@@ -2067,8 +2218,6 @@
|
||||
|
||||
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"sass/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"set-value/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
|
||||
|
||||
"shapefile/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
@@ -2077,23 +2226,25 @@
|
||||
|
||||
"split-string/extend-shallow": ["extend-shallow@3.0.2", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="],
|
||||
|
||||
"string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
"string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
|
||||
"sucrase/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
|
||||
"tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
"tailwindcss/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
|
||||
|
||||
"topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"topojson-server/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"vite/postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
|
||||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
"wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
@@ -2107,8 +2258,12 @@
|
||||
|
||||
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q=="],
|
||||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@0.5.4", "", { "dependencies": { "@floating-ui/core": "^0.7.3" } }, "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg=="],
|
||||
|
||||
"@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.0.0", "", { "dependencies": { "@babel/runtime": "^7.13.10" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0" } }, "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA=="],
|
||||
@@ -2127,13 +2282,11 @@
|
||||
|
||||
"d3/d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
"leva/react-dropzone/file-selector": ["file-selector@0.5.0", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA=="],
|
||||
|
||||
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||
|
||||
"sass/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
"ora/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"simplify-geojson/concat-stream/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="],
|
||||
|
||||
@@ -2141,9 +2294,19 @@
|
||||
|
||||
"string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"sucrase/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"sucrase/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
@@ -2163,6 +2326,10 @@
|
||||
|
||||
"simplify-geojson/concat-stream/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
|
||||
|
||||
"sucrase/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
"sucrase/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"@babel/plugin-transform-runtime/@babel/helper-module-imports/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Card, Spin } from '@douyinfe/semi-ui';
|
||||
import SettingsGeneralPayment from '../../pages/Setting/Payment/SettingsGeneralPayment';
|
||||
import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentGateway';
|
||||
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe';
|
||||
import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem';
|
||||
import { API, showError, toBoolean } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -142,6 +143,9 @@ const PaymentSetting = () => {
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsPaymentGatewayStripe options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
<Card style={{ marginTop: '10px' }}>
|
||||
<SettingsPaymentGatewayCreem options={inputs} refresh={onRefresh} />
|
||||
</Card>
|
||||
</Spin>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -46,6 +46,7 @@ const RatioSetting = () => {
|
||||
DefaultUseAutoGroup: false,
|
||||
ExposeRatioEnabled: false,
|
||||
UserUsableGroups: '',
|
||||
'group_ratio_setting.group_special_usable_group': '',
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -57,17 +58,7 @@ const RatioSetting = () => {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
if (
|
||||
item.key === 'ModelRatio' ||
|
||||
item.key === 'GroupRatio' ||
|
||||
item.key === 'GroupGroupRatio' ||
|
||||
item.key === 'AutoGroups' ||
|
||||
item.key === 'UserUsableGroups' ||
|
||||
item.key === 'CompletionRatio' ||
|
||||
item.key === 'ModelPrice' ||
|
||||
item.key === 'CacheRatio' ||
|
||||
item.key === 'ImageRatio' ||
|
||||
item.key === 'AudioRatio' ||
|
||||
item.key === 'AudioCompletionRatio'
|
||||
item.value.startsWith('{') || item.value.startsWith('[')
|
||||
) {
|
||||
try {
|
||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
|
||||
@@ -107,6 +107,8 @@ function type2secretPrompt(type) {
|
||||
return '按照如下格式输入:AppId|SecretId|SecretKey';
|
||||
case 33:
|
||||
return '按照如下格式输入:Ak|Sk|Region';
|
||||
case 45:
|
||||
return '请输入渠道对应的鉴权密钥, 豆包语音输入:AppId|AccessToken';
|
||||
case 50:
|
||||
return '按照如下格式输入: AccessKey|SecretKey, 如果上游是New API,则直接输ApiKey';
|
||||
case 51:
|
||||
|
||||
@@ -137,14 +137,12 @@ const EditTokenModal = (props) => {
|
||||
if (statusState?.status?.default_use_auto_group) {
|
||||
if (localGroupOptions.some((group) => group.value === 'auto')) {
|
||||
localGroupOptions.sort((a, b) => (a.value === 'auto' ? -1 : 1));
|
||||
} else {
|
||||
localGroupOptions.unshift({ label: t('自动选择'), value: 'auto' });
|
||||
}
|
||||
}
|
||||
setGroups(localGroupOptions);
|
||||
if (statusState?.status?.default_use_auto_group && formApiRef.current) {
|
||||
formApiRef.current.setValue('group', 'auto');
|
||||
}
|
||||
// if (statusState?.status?.default_use_auto_group && formApiRef.current) {
|
||||
// formApiRef.current.setValue('group', 'auto');
|
||||
// }
|
||||
} else {
|
||||
showError(t(message));
|
||||
}
|
||||
|
||||
@@ -52,6 +52,9 @@ const RechargeCard = ({
|
||||
t,
|
||||
enableOnlineTopUp,
|
||||
enableStripeTopUp,
|
||||
enableCreemTopUp,
|
||||
creemProducts,
|
||||
creemPreTopUp,
|
||||
presetAmounts,
|
||||
selectedPreset,
|
||||
selectPresetAmount,
|
||||
@@ -84,6 +87,7 @@ const RechargeCard = ({
|
||||
const onlineFormApiRef = useRef(null);
|
||||
const redeemFormApiRef = useRef(null);
|
||||
const showAmountSkeleton = useMinimumLoadingTime(amountLoading);
|
||||
console.log(' enabled screem ?', enableCreemTopUp, ' products ?', creemProducts);
|
||||
return (
|
||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||
{/* 卡片头部 */}
|
||||
@@ -216,7 +220,7 @@ const RechargeCard = ({
|
||||
<div className='py-8 flex justify-center'>
|
||||
<Spin size='large' />
|
||||
</div>
|
||||
) : enableOnlineTopUp || enableStripeTopUp ? (
|
||||
) : enableOnlineTopUp || enableStripeTopUp || enableCreemTopUp ? (
|
||||
<Form
|
||||
getFormApi={(api) => (onlineFormApiRef.current = api)}
|
||||
initValues={{ topUpCount: topUpCount }}
|
||||
@@ -447,7 +451,7 @@ const RechargeCard = ({
|
||||
style={{ margin: '0 0 8px 0' }}
|
||||
>
|
||||
<Coins size={18} />
|
||||
{formatLargeNumber(preset.value)} $
|
||||
{formatLargeNumber(displayValue)} {symbol}
|
||||
{hasDiscount && (
|
||||
<Tag style={{ marginLeft: 4 }} color='green'>
|
||||
{t('折').includes('off')
|
||||
@@ -480,6 +484,32 @@ const RechargeCard = ({
|
||||
</div>
|
||||
</Form.Slot>
|
||||
)}
|
||||
|
||||
{/* Creem 充值区域 */}
|
||||
{enableCreemTopUp && creemProducts.length > 0 && (
|
||||
<Form.Slot label={t('Creem 充值')}>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3'>
|
||||
{creemProducts.map((product, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
onClick={() => creemPreTopUp(product)}
|
||||
className='cursor-pointer !rounded-2xl transition-all hover:shadow-md border-gray-200 hover:border-gray-300'
|
||||
bodyStyle={{ textAlign: 'center', padding: '16px' }}
|
||||
>
|
||||
<div className='font-medium text-lg mb-2'>
|
||||
{product.name}
|
||||
</div>
|
||||
<div className='text-sm text-gray-600 mb-2'>
|
||||
{t('充值额度')}: {product.quota}
|
||||
</div>
|
||||
<div className='text-lg font-semibold text-blue-600'>
|
||||
{product.currency === 'EUR' ? '€' : '$'}{product.price}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Form.Slot>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
) : (
|
||||
|
||||
@@ -63,6 +63,12 @@ const TopUp = () => {
|
||||
);
|
||||
const [statusLoading, setStatusLoading] = useState(true);
|
||||
|
||||
// Creem 相关状态
|
||||
const [creemProducts, setCreemProducts] = useState([]);
|
||||
const [enableCreemTopUp, setEnableCreemTopUp] = useState(false);
|
||||
const [creemOpen, setCreemOpen] = useState(false);
|
||||
const [selectedCreemProduct, setSelectedCreemProduct] = useState(null);
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [payWay, setPayWay] = useState('');
|
||||
@@ -248,6 +254,55 @@ const TopUp = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const creemPreTopUp = async (product) => {
|
||||
if (!enableCreemTopUp) {
|
||||
showError(t('管理员未开启 Creem 充值!'));
|
||||
return;
|
||||
}
|
||||
setSelectedCreemProduct(product);
|
||||
setCreemOpen(true);
|
||||
};
|
||||
|
||||
const onlineCreemTopUp = async () => {
|
||||
if (!selectedCreemProduct) {
|
||||
showError(t('请选择产品'));
|
||||
return;
|
||||
}
|
||||
// Validate product has required fields
|
||||
if (!selectedCreemProduct.productId) {
|
||||
showError(t('产品配置错误,请联系管理员'));
|
||||
return;
|
||||
}
|
||||
setConfirmLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/user/creem/pay', {
|
||||
product_id: selectedCreemProduct.productId,
|
||||
payment_method: 'creem',
|
||||
});
|
||||
if (res !== undefined) {
|
||||
const { message, data } = res.data;
|
||||
if (message === 'success') {
|
||||
processCreemCallback(data);
|
||||
} else {
|
||||
showError(data);
|
||||
}
|
||||
} else {
|
||||
showError(res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
showError(t('支付请求失败'));
|
||||
} finally {
|
||||
setCreemOpen(false);
|
||||
setConfirmLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processCreemCallback = (data) => {
|
||||
// 与 Stripe 保持一致的实现方式
|
||||
window.open(data.checkout_url, '_blank');
|
||||
};
|
||||
|
||||
const getUserQuota = async () => {
|
||||
let res = await API.get(`/api/user/self`);
|
||||
const { success, message, data } = res.data;
|
||||
@@ -322,6 +377,7 @@ const TopUp = () => {
|
||||
setPayMethods(payMethods);
|
||||
const enableStripeTopUp = data.enable_stripe_topup || false;
|
||||
const enableOnlineTopUp = data.enable_online_topup || false;
|
||||
const enableCreemTopUp = data.enable_creem_topup || false;
|
||||
const minTopUpValue = enableOnlineTopUp
|
||||
? data.min_topup
|
||||
: enableStripeTopUp
|
||||
@@ -329,9 +385,20 @@ const TopUp = () => {
|
||||
: 1;
|
||||
setEnableOnlineTopUp(enableOnlineTopUp);
|
||||
setEnableStripeTopUp(enableStripeTopUp);
|
||||
setEnableCreemTopUp(enableCreemTopUp);
|
||||
setMinTopUp(minTopUpValue);
|
||||
setTopUpCount(minTopUpValue);
|
||||
|
||||
// 设置 Creem 产品
|
||||
try {
|
||||
console.log(' data is ?', data);
|
||||
console.log(' creem products is ?', data.creem_products);
|
||||
const products = JSON.parse(data.creem_products || '[]');
|
||||
setCreemProducts(products);
|
||||
} catch (e) {
|
||||
setCreemProducts([]);
|
||||
}
|
||||
|
||||
// 如果没有自定义充值数量选项,根据最小充值金额生成预设充值额度选项
|
||||
if (topupInfo.amount_options.length === 0) {
|
||||
setPresetAmounts(generatePresetAmounts(minTopUpValue));
|
||||
@@ -500,6 +567,11 @@ const TopUp = () => {
|
||||
setOpenHistory(false);
|
||||
};
|
||||
|
||||
const handleCreemCancel = () => {
|
||||
setCreemOpen(false);
|
||||
setSelectedCreemProduct(null);
|
||||
};
|
||||
|
||||
// 选择预设充值额度
|
||||
const selectPresetAmount = (preset) => {
|
||||
setTopUpCount(preset.value);
|
||||
@@ -563,6 +635,33 @@ const TopUp = () => {
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* Creem 充值确认模态框 */}
|
||||
<Modal
|
||||
title={t('确定要充值 $')}
|
||||
visible={creemOpen}
|
||||
onOk={onlineCreemTopUp}
|
||||
onCancel={handleCreemCancel}
|
||||
maskClosable={false}
|
||||
size='small'
|
||||
centered
|
||||
confirmLoading={confirmLoading}
|
||||
>
|
||||
{selectedCreemProduct && (
|
||||
<>
|
||||
<p>
|
||||
{t('产品名称')}:{selectedCreemProduct.name}
|
||||
</p>
|
||||
<p>
|
||||
{t('价格')}:{selectedCreemProduct.currency === 'EUR' ? '€' : '$'}{selectedCreemProduct.price}
|
||||
</p>
|
||||
<p>
|
||||
{t('充值额度')}:{selectedCreemProduct.quota}
|
||||
</p>
|
||||
<p>{t('是否确认充值?')}</p>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* 用户信息头部 */}
|
||||
<div className='space-y-6'>
|
||||
<div className='grid grid-cols-1 lg:grid-cols-12 gap-6'>
|
||||
@@ -572,6 +671,9 @@ const TopUp = () => {
|
||||
t={t}
|
||||
enableOnlineTopUp={enableOnlineTopUp}
|
||||
enableStripeTopUp={enableStripeTopUp}
|
||||
enableCreemTopUp={enableCreemTopUp}
|
||||
creemProducts={creemProducts}
|
||||
creemPreTopUp={creemPreTopUp}
|
||||
presetAmounts={presetAmounts}
|
||||
selectedPreset={selectedPreset}
|
||||
selectPresetAmount={selectPresetAmount}
|
||||
|
||||
@@ -2071,6 +2071,35 @@
|
||||
"默认区域,如: us-central1": "Default region, e.g.: us-central1",
|
||||
"默认折叠侧边栏": "Default collapse sidebar",
|
||||
"默认测试模型": "Default Test Model",
|
||||
"默认补全倍率": "Default completion ratio"
|
||||
"默认补全倍率": "Default completion ratio",
|
||||
"选择充值套餐": "Choose a top-up package",
|
||||
"Creem 设置": "Creem Setting",
|
||||
"Creem 充值": "Creem Recharge",
|
||||
"Creem 介绍": "Creem is the payment partner you always deserved, we strive for simplicity and straightforwardness on our APIs.",
|
||||
"Creem Setting Tips": "Creem only supports preset fixed-amount products. These products and their prices need to be created and configured in advance on the Creem website, so custom dynamic amount top-ups are not supported. Configure the product name and price on Creem, obtain the Product Id, and then fill it in for the product below. Set the top-up amount and display price for this product in the new API.",
|
||||
"Webhook 密钥": "Webhook Secret",
|
||||
"测试模式": "Test Mode",
|
||||
"Creem API 密钥,敏感信息不显示": "Creem API key, sensitive information not displayed",
|
||||
"用于验证回调 new-api 的 webhook 请求的密钥,敏感信息不显示": "The key used to validate webhook requests for the callback new-api, sensitive information is not displayed.",
|
||||
"启用后将使用 Creem Test Mode": "",
|
||||
"展示价格": "Display Pricing",
|
||||
"Recharge Quota": "Recharge Quota",
|
||||
"产品配置": "Product Configuration",
|
||||
"产品名称": "Product Name",
|
||||
"产品ID": "Product ID",
|
||||
"暂无产品配置": "No product configuration",
|
||||
"更新 Creem 设置": "Update Creem Settings",
|
||||
"编辑产品": "Edit Product",
|
||||
"添加产品": "Add Product",
|
||||
"例如:基础套餐": "e.g.: Basic Package",
|
||||
"例如:prod_6I8rBerHpPxyoiU9WK4kot": "e.g.: prod_6I8rBerHpPxyoiU9WK4kot",
|
||||
"货币": "Currency",
|
||||
"欧元": "EUR",
|
||||
"USD (美元)": "USD (US Dollar)",
|
||||
"EUR (欧元)": "EUR (Euro)",
|
||||
"例如:4.99": "e.g.: 4.99",
|
||||
"例如:100000": "e.g.: 100000",
|
||||
"请填写完整的产品信息": "Please fill in complete product information",
|
||||
"产品ID已存在": "Product ID already exists"
|
||||
}
|
||||
}
|
||||
}
|
||||
2076
web/src/i18n/locales/ja.json
Normal file
2076
web/src/i18n/locales/ja.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2062,6 +2062,8 @@
|
||||
"默认区域,如: us-central1": "默认区域,如: us-central1",
|
||||
"默认折叠侧边栏": "默认折叠侧边栏",
|
||||
"默认测试模型": "默认测试模型",
|
||||
"默认补全倍率": "默认补全倍率"
|
||||
"默认补全倍率": "默认补全倍率",
|
||||
"Creem 介绍": "Creem 是一个简单的支付处理平台,支持固定金额产品销售,以及订阅销售。",
|
||||
"Creem Setting Tips": "Creem 只支持预设的固定金额产品,这产品以及价格需要提前在Creem网站内创建配置,所以不支持自定义动态金额充值。在Creem端配置产品的名字以及价格,获取Product Id 后填到下面的产品,在new-api为该产品设置充值额度,以及展示价格。"
|
||||
}
|
||||
}
|
||||
}
|
||||
385
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx
Normal file
385
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx
Normal file
@@ -0,0 +1,385 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Banner,
|
||||
Button,
|
||||
Form,
|
||||
Row,
|
||||
Col,
|
||||
Typography,
|
||||
Spin,
|
||||
Table,
|
||||
Modal,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
} from '@douyinfe/semi-ui';
|
||||
const { Text } = Typography;
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, Trash2 } from 'lucide-react';
|
||||
|
||||
export default function SettingsPaymentGatewayCreem(props) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
CreemApiKey: '',
|
||||
CreemWebhookSecret: '',
|
||||
CreemProducts: '[]',
|
||||
CreemTestMode: false,
|
||||
});
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
const [products, setProducts] = useState([]);
|
||||
const [showProductModal, setShowProductModal] = useState(false);
|
||||
const [editingProduct, setEditingProduct] = useState(null);
|
||||
const [productForm, setProductForm] = useState({
|
||||
name: '',
|
||||
productId: '',
|
||||
price: 0,
|
||||
quota: 0,
|
||||
currency: 'USD',
|
||||
});
|
||||
const formApiRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.options && formApiRef.current) {
|
||||
const currentInputs = {
|
||||
CreemApiKey: props.options.CreemApiKey || '',
|
||||
CreemWebhookSecret: props.options.CreemWebhookSecret || '',
|
||||
CreemProducts: props.options.CreemProducts || '[]',
|
||||
CreemTestMode: props.options.CreemTestMode === 'true',
|
||||
};
|
||||
setInputs(currentInputs);
|
||||
setOriginInputs({ ...currentInputs });
|
||||
formApiRef.current.setValues(currentInputs);
|
||||
|
||||
// Parse products
|
||||
try {
|
||||
const parsedProducts = JSON.parse(currentInputs.CreemProducts);
|
||||
setProducts(parsedProducts);
|
||||
} catch (e) {
|
||||
setProducts([]);
|
||||
}
|
||||
}
|
||||
}, [props.options]);
|
||||
|
||||
const handleFormChange = (values) => {
|
||||
setInputs(values);
|
||||
};
|
||||
|
||||
const submitCreemSetting = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const options = [];
|
||||
|
||||
if (inputs.CreemApiKey && inputs.CreemApiKey !== '') {
|
||||
options.push({ key: 'CreemApiKey', value: inputs.CreemApiKey });
|
||||
}
|
||||
|
||||
if (inputs.CreemWebhookSecret && inputs.CreemWebhookSecret !== '') {
|
||||
options.push({ key: 'CreemWebhookSecret', value: inputs.CreemWebhookSecret });
|
||||
}
|
||||
|
||||
// Save test mode setting
|
||||
options.push({ key: 'CreemTestMode', value: inputs.CreemTestMode ? 'true' : 'false' });
|
||||
|
||||
// Save products as JSON string
|
||||
options.push({ key: 'CreemProducts', value: JSON.stringify(products) });
|
||||
|
||||
// 发送请求
|
||||
const requestQueue = options.map(opt =>
|
||||
API.put('/api/option/', {
|
||||
key: opt.key,
|
||||
value: opt.value,
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(requestQueue);
|
||||
|
||||
// 检查所有请求是否成功
|
||||
const errorResults = results.filter(res => !res.data.success);
|
||||
if (errorResults.length > 0) {
|
||||
errorResults.forEach(res => {
|
||||
showError(res.data.message);
|
||||
});
|
||||
} else {
|
||||
showSuccess(t('更新成功'));
|
||||
// 更新本地存储的原始值
|
||||
setOriginInputs({ ...inputs });
|
||||
props.refresh?.();
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('更新失败'));
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const openProductModal = (product = null) => {
|
||||
if (product) {
|
||||
setEditingProduct(product);
|
||||
setProductForm({ ...product });
|
||||
} else {
|
||||
setEditingProduct(null);
|
||||
setProductForm({
|
||||
name: '',
|
||||
productId: '',
|
||||
price: 0,
|
||||
quota: 0,
|
||||
currency: 'USD',
|
||||
});
|
||||
}
|
||||
setShowProductModal(true);
|
||||
};
|
||||
|
||||
const closeProductModal = () => {
|
||||
setShowProductModal(false);
|
||||
setEditingProduct(null);
|
||||
setProductForm({
|
||||
name: '',
|
||||
productId: '',
|
||||
price: 0,
|
||||
quota: 0,
|
||||
currency: 'USD',
|
||||
});
|
||||
};
|
||||
|
||||
const saveProduct = () => {
|
||||
if (!productForm.name || !productForm.productId || productForm.price <= 0 || productForm.quota <= 0 || !productForm.currency) {
|
||||
showError(t('请填写完整的产品信息'));
|
||||
return;
|
||||
}
|
||||
|
||||
let newProducts = [...products];
|
||||
if (editingProduct) {
|
||||
// 编辑现有产品
|
||||
const index = newProducts.findIndex(p => p.productId === editingProduct.productId);
|
||||
if (index !== -1) {
|
||||
newProducts[index] = { ...productForm };
|
||||
}
|
||||
} else {
|
||||
// 添加新产品
|
||||
if (newProducts.find(p => p.productId === productForm.productId)) {
|
||||
showError(t('产品ID已存在'));
|
||||
return;
|
||||
}
|
||||
newProducts.push({ ...productForm });
|
||||
}
|
||||
|
||||
setProducts(newProducts);
|
||||
closeProductModal();
|
||||
};
|
||||
|
||||
const deleteProduct = (productId) => {
|
||||
const newProducts = products.filter(p => p.productId !== productId);
|
||||
setProducts(newProducts);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('产品名称'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: t('产品ID'),
|
||||
dataIndex: 'productId',
|
||||
key: 'productId',
|
||||
},
|
||||
{
|
||||
title: t('展示价格'),
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
render: (price, record) => `${record.currency === 'EUR' ? '€' : '$'}${price}`,
|
||||
},
|
||||
{
|
||||
title: t('充值额度'),
|
||||
dataIndex: 'quota',
|
||||
key: 'quota',
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
type='tertiary'
|
||||
size='small'
|
||||
onClick={() => openProductModal(record)}
|
||||
>
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
<Button
|
||||
type='danger'
|
||||
theme='borderless'
|
||||
size='small'
|
||||
icon={<Trash2 size={14} />}
|
||||
onClick={() => deleteProduct(record.productId)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
initValues={inputs}
|
||||
onValueChange={handleFormChange}
|
||||
getFormApi={(api) => (formApiRef.current = api)}
|
||||
>
|
||||
<Form.Section text={t('Creem 设置')}>
|
||||
<Text>
|
||||
{t('Creem 介绍')}
|
||||
<a
|
||||
href='https://creem.io'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>Creem Official Site</a>
|
||||
<br />
|
||||
</Text>
|
||||
<Banner
|
||||
type='info'
|
||||
description={t('Creem Setting Tips')}
|
||||
/>
|
||||
|
||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='CreemApiKey'
|
||||
label={t('API 密钥')}
|
||||
placeholder={t('Creem API 密钥,敏感信息不显示')}
|
||||
type='password'
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Input
|
||||
field='CreemWebhookSecret'
|
||||
label={t('Webhook 密钥')}
|
||||
placeholder={t('用于验证回调 new-api 的 webhook 请求的密钥,敏感信息不显示')}
|
||||
type='password'
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||
<Form.Switch
|
||||
field='CreemTestMode'
|
||||
label={t('测试模式')}
|
||||
extraText={t('启用后将使用 Creem Test Mode')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<Text strong>{t('产品配置')}</Text>
|
||||
<Button
|
||||
type='primary'
|
||||
icon={<Plus size={16} />}
|
||||
onClick={() => openProductModal()}
|
||||
>
|
||||
{t('添加产品')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={products}
|
||||
pagination={false}
|
||||
empty={
|
||||
<div className='text-center py-8'>
|
||||
<Text type='tertiary'>{t('暂无产品配置')}</Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button onClick={submitCreemSetting} style={{ marginTop: 16 }}>
|
||||
{t('更新 Creem 设置')}
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
|
||||
{/* 产品配置模态框 */}
|
||||
<Modal
|
||||
title={editingProduct ? t('编辑产品') : t('添加产品')}
|
||||
visible={showProductModal}
|
||||
onOk={saveProduct}
|
||||
onCancel={closeProductModal}
|
||||
maskClosable={false}
|
||||
size='small'
|
||||
centered
|
||||
>
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<Text strong className='block mb-2'>
|
||||
{t('产品名称')}
|
||||
</Text>
|
||||
<Input
|
||||
value={productForm.name}
|
||||
onChange={(value) => setProductForm({ ...productForm, name: value })}
|
||||
placeholder={t('例如:基础套餐')}
|
||||
size='large'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong className='block mb-2'>
|
||||
{t('产品ID')}
|
||||
</Text>
|
||||
<Input
|
||||
value={productForm.productId}
|
||||
onChange={(value) => setProductForm({ ...productForm, productId: value })}
|
||||
placeholder={t('例如:prod_6I8rBerHpPxyoiU9WK4kot')}
|
||||
size='large'
|
||||
disabled={!!editingProduct}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong className='block mb-2'>
|
||||
{t('货币')}
|
||||
</Text>
|
||||
<Select
|
||||
value={productForm.currency}
|
||||
onChange={(value) => setProductForm({ ...productForm, currency: value })}
|
||||
size='large'
|
||||
className='w-full'
|
||||
>
|
||||
<Select.Option value='USD'>{t('USD (美元)')}</Select.Option>
|
||||
<Select.Option value='EUR'>{t('EUR (欧元)')}</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong className='block mb-2'>
|
||||
{t('价格')} ({productForm.currency === 'EUR' ? t('欧元') : t('美元')})
|
||||
</Text>
|
||||
<InputNumber
|
||||
value={productForm.price}
|
||||
onChange={(value) => setProductForm({ ...productForm, price: value })}
|
||||
placeholder={t('例如:4.99')}
|
||||
min={0.01}
|
||||
precision={2}
|
||||
size='large'
|
||||
className='w-full'
|
||||
defaultValue={4.49}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong className='block mb-2'>
|
||||
{t('充值额度')}
|
||||
</Text>
|
||||
<InputNumber
|
||||
value={productForm.quota}
|
||||
onChange={(value) => setProductForm({ ...productForm, quota: value })}
|
||||
placeholder={t('例如:100000')}
|
||||
min={1}
|
||||
precision={0}
|
||||
size='large'
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,7 @@ export default function GroupRatioSettings(props) {
|
||||
GroupRatio: '',
|
||||
UserUsableGroups: '',
|
||||
GroupGroupRatio: '',
|
||||
'group_ratio_setting.group_special_usable_group': '',
|
||||
AutoGroups: '',
|
||||
DefaultUseAutoGroup: false,
|
||||
});
|
||||
@@ -185,6 +186,30 @@ export default function GroupRatioSettings(props) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
label={t('分组特殊可用分组')}
|
||||
placeholder={t('为一个 JSON 文本')}
|
||||
extraText={t(
|
||||
'键为用户分组名称,值为操作映射对象。内层键以"+:"开头表示添加指定分组(键值为分组名称,值为描述),以"-:"开头表示移除指定分组(键值为分组名称),不带前缀的键直接添加该分组。例如:{"vip": {"+:premium": "高级分组", "special": "特殊分组", "-:default": "默认分组"}},表示 vip 分组的用户可以使用 premium 和 special 分组,同时移除 default 分组的访问权限',
|
||||
)}
|
||||
field={'group_ratio_setting.group_special_usable_group'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, 'group_ratio_setting.group_special_usable_group': value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
|
||||
Reference in New Issue
Block a user