Merge remote-tracking branch 'remotes/up-origin/main' into feat-channel-block-edit
This commit is contained in:
commit
8ed3e4f034
8
.github/workflows/linux-release.yml
vendored
8
.github/workflows/linux-release.yml
vendored
@ -38,21 +38,21 @@ jobs:
|
|||||||
- name: Build Backend (amd64)
|
- name: Build Backend (amd64)
|
||||||
run: |
|
run: |
|
||||||
go mod download
|
go mod download
|
||||||
go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o one-api
|
go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api
|
||||||
|
|
||||||
- name: Build Backend (arm64)
|
- name: Build Backend (arm64)
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu
|
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||||
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o one-api-arm64
|
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api-arm64
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
one-api
|
new-api
|
||||||
one-api-arm64
|
new-api-arm64
|
||||||
draft: true
|
draft: true
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
env:
|
env:
|
||||||
|
|||||||
4
.github/workflows/macos-release.yml
vendored
4
.github/workflows/macos-release.yml
vendored
@ -39,12 +39,12 @@ jobs:
|
|||||||
- name: Build Backend
|
- name: Build Backend
|
||||||
run: |
|
run: |
|
||||||
go mod download
|
go mod download
|
||||||
go build -ldflags "-X 'one-api/common.Version=$(git describe --tags)'" -o one-api-macos
|
go build -ldflags "-X 'one-api/common.Version=$(git describe --tags)'" -o new-api-macos
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
files: one-api-macos
|
files: new-api-macos
|
||||||
draft: true
|
draft: true
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
env:
|
env:
|
||||||
|
|||||||
4
.github/workflows/windows-release.yml
vendored
4
.github/workflows/windows-release.yml
vendored
@ -41,12 +41,12 @@ jobs:
|
|||||||
- name: Build Backend
|
- name: Build Backend
|
||||||
run: |
|
run: |
|
||||||
go mod download
|
go mod download
|
||||||
go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)'" -o one-api.exe
|
go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)'" -o new-api.exe
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
files: one-api.exe
|
files: new-api.exe
|
||||||
draft: true
|
draft: true
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
env:
|
env:
|
||||||
|
|||||||
@ -2,9 +2,10 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SysLog(s string) {
|
func SysLog(s string) {
|
||||||
@ -22,3 +23,33 @@ func FatalLog(v ...any) {
|
|||||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
|
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LogStartupSuccess(startTime time.Time, port string) {
|
||||||
|
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
durationMs := duration.Milliseconds()
|
||||||
|
|
||||||
|
// Get network IPs
|
||||||
|
networkIps := GetNetworkIps()
|
||||||
|
|
||||||
|
// Print blank line for spacing
|
||||||
|
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||||
|
|
||||||
|
// Print the main success message
|
||||||
|
fmt.Fprintf(gin.DefaultWriter, " \033[32m%s %s\033[0m ready in %d ms\n", SystemName, Version, durationMs)
|
||||||
|
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||||
|
|
||||||
|
// Skip fancy startup message in container environments
|
||||||
|
if !IsRunningInContainer() {
|
||||||
|
// Print local URL
|
||||||
|
fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mLocal:\033[0m http://localhost:%s/\n", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print network URLs
|
||||||
|
for _, ip := range networkIps {
|
||||||
|
fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mNetwork:\033[0m http://%s:%s/\n", ip, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print blank line for spacing
|
||||||
|
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||||
|
}
|
||||||
|
|||||||
@ -68,6 +68,78 @@ func GetIp() (ip string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetNetworkIps() []string {
|
||||||
|
var networkIps []string
|
||||||
|
ips, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return networkIps
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range ips {
|
||||||
|
if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||||
|
if ipNet.IP.To4() != nil {
|
||||||
|
ip := ipNet.IP.String()
|
||||||
|
// Include common private network ranges
|
||||||
|
if strings.HasPrefix(ip, "10.") ||
|
||||||
|
strings.HasPrefix(ip, "172.") ||
|
||||||
|
strings.HasPrefix(ip, "192.168.") {
|
||||||
|
networkIps = append(networkIps, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return networkIps
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRunningInContainer detects if the application is running inside a container
|
||||||
|
func IsRunningInContainer() bool {
|
||||||
|
// Method 1: Check for .dockerenv file (Docker containers)
|
||||||
|
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Check cgroup for container indicators
|
||||||
|
if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
|
||||||
|
content := string(data)
|
||||||
|
if strings.Contains(content, "docker") ||
|
||||||
|
strings.Contains(content, "containerd") ||
|
||||||
|
strings.Contains(content, "kubepods") ||
|
||||||
|
strings.Contains(content, "/lxc/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: Check environment variables commonly set by container runtimes
|
||||||
|
containerEnvVars := []string{
|
||||||
|
"KUBERNETES_SERVICE_HOST",
|
||||||
|
"DOCKER_CONTAINER",
|
||||||
|
"container",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, envVar := range containerEnvVars {
|
||||||
|
if os.Getenv(envVar) != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 4: Check if init process is not the traditional init
|
||||||
|
if data, err := os.ReadFile("/proc/1/comm"); err == nil {
|
||||||
|
comm := strings.TrimSpace(string(data))
|
||||||
|
// In containers, process 1 is often not "init" or "systemd"
|
||||||
|
if comm != "init" && comm != "systemd" {
|
||||||
|
// Additional check: if it's a common container entrypoint
|
||||||
|
if strings.Contains(comm, "docker") ||
|
||||||
|
strings.Contains(comm, "containerd") ||
|
||||||
|
strings.Contains(comm, "runc") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
var sizeKB = 1024
|
var sizeKB = 1024
|
||||||
var sizeMB = sizeKB * 1024
|
var sizeMB = sizeKB * 1024
|
||||||
var sizeGB = sizeMB * 1024
|
var sizeGB = sizeMB * 1024
|
||||||
|
|||||||
@ -11,8 +11,10 @@ const (
|
|||||||
SunoActionMusic = "MUSIC"
|
SunoActionMusic = "MUSIC"
|
||||||
SunoActionLyrics = "LYRICS"
|
SunoActionLyrics = "LYRICS"
|
||||||
|
|
||||||
TaskActionGenerate = "generate"
|
TaskActionGenerate = "generate"
|
||||||
TaskActionTextGenerate = "textGenerate"
|
TaskActionTextGenerate = "textGenerate"
|
||||||
|
TaskActionFirstTailGenerate = "firstTailGenerate"
|
||||||
|
TaskActionReferenceGenerate = "referenceGenerate"
|
||||||
)
|
)
|
||||||
|
|
||||||
var SunoModel2Action = map[string]string{
|
var SunoModel2Action = map[string]string{
|
||||||
|
|||||||
@ -188,6 +188,8 @@ func FetchUpstreamModels(c *gin.Context) {
|
|||||||
url = fmt.Sprintf("%s/v1beta/openai/models", baseURL) // Remove key in url since we need to use AuthHeader
|
url = fmt.Sprintf("%s/v1beta/openai/models", baseURL) // Remove key in url since we need to use AuthHeader
|
||||||
case constant.ChannelTypeAli:
|
case constant.ChannelTypeAli:
|
||||||
url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
|
url = fmt.Sprintf("%s/compatible-mode/v1/models", baseURL)
|
||||||
|
case constant.ChannelTypeZhipu_v4:
|
||||||
|
url = fmt.Sprintf("%s/api/paas/v4/models", baseURL)
|
||||||
default:
|
default:
|
||||||
url = fmt.Sprintf("%s/v1/models", baseURL)
|
url = fmt.Sprintf("%s/v1/models", baseURL)
|
||||||
}
|
}
|
||||||
@ -1101,8 +1103,8 @@ func CopyChannel(c *gin.Context) {
|
|||||||
// MultiKeyManageRequest represents the request for multi-key management operations
|
// MultiKeyManageRequest represents the request for multi-key management operations
|
||||||
type MultiKeyManageRequest struct {
|
type MultiKeyManageRequest struct {
|
||||||
ChannelId int `json:"channel_id"`
|
ChannelId int `json:"channel_id"`
|
||||||
Action string `json:"action"` // "disable_key", "enable_key", "delete_disabled_keys", "get_key_status"
|
Action string `json:"action"` // "disable_key", "enable_key", "delete_key", "delete_disabled_keys", "get_key_status"
|
||||||
KeyIndex *int `json:"key_index,omitempty"` // for disable_key and enable_key actions
|
KeyIndex *int `json:"key_index,omitempty"` // for disable_key, enable_key, and delete_key actions
|
||||||
Page int `json:"page,omitempty"` // for get_key_status pagination
|
Page int `json:"page,omitempty"` // for get_key_status pagination
|
||||||
PageSize int `json:"page_size,omitempty"` // for get_key_status pagination
|
PageSize int `json:"page_size,omitempty"` // for get_key_status pagination
|
||||||
Status *int `json:"status,omitempty"` // for get_key_status filtering: 1=enabled, 2=manual_disabled, 3=auto_disabled, nil=all
|
Status *int `json:"status,omitempty"` // for get_key_status filtering: 1=enabled, 2=manual_disabled, 3=auto_disabled, nil=all
|
||||||
@ -1430,6 +1432,86 @@ func ManageMultiKeys(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
||||||
|
case "delete_key":
|
||||||
|
if request.KeyIndex == nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "未指定要删除的密钥索引",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keyIndex := *request.KeyIndex
|
||||||
|
if keyIndex < 0 || keyIndex >= channel.ChannelInfo.MultiKeySize {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "密钥索引超出范围",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := channel.GetKeys()
|
||||||
|
var remainingKeys []string
|
||||||
|
var newStatusList = make(map[int]int)
|
||||||
|
var newDisabledTime = make(map[int]int64)
|
||||||
|
var newDisabledReason = make(map[int]string)
|
||||||
|
|
||||||
|
newIndex := 0
|
||||||
|
for i, key := range keys {
|
||||||
|
// 跳过要删除的密钥
|
||||||
|
if i == keyIndex {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingKeys = append(remainingKeys, key)
|
||||||
|
|
||||||
|
// 保留其他密钥的状态信息,重新索引
|
||||||
|
if channel.ChannelInfo.MultiKeyStatusList != nil {
|
||||||
|
if status, exists := channel.ChannelInfo.MultiKeyStatusList[i]; exists && status != 1 {
|
||||||
|
newStatusList[newIndex] = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if channel.ChannelInfo.MultiKeyDisabledTime != nil {
|
||||||
|
if t, exists := channel.ChannelInfo.MultiKeyDisabledTime[i]; exists {
|
||||||
|
newDisabledTime[newIndex] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if channel.ChannelInfo.MultiKeyDisabledReason != nil {
|
||||||
|
if r, exists := channel.ChannelInfo.MultiKeyDisabledReason[i]; exists {
|
||||||
|
newDisabledReason[newIndex] = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(remainingKeys) == 0 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "不能删除最后一个密钥",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update channel with remaining keys
|
||||||
|
channel.Key = strings.Join(remainingKeys, "\n")
|
||||||
|
channel.ChannelInfo.MultiKeySize = len(remainingKeys)
|
||||||
|
channel.ChannelInfo.MultiKeyStatusList = newStatusList
|
||||||
|
channel.ChannelInfo.MultiKeyDisabledTime = newDisabledTime
|
||||||
|
channel.ChannelInfo.MultiKeyDisabledReason = newDisabledReason
|
||||||
|
|
||||||
|
err = channel.Update()
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
model.InitChannelCache()
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "密钥已删除",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
case "delete_disabled_keys":
|
case "delete_disabled_keys":
|
||||||
keys := channel.GetKeys()
|
keys := channel.GetKeys()
|
||||||
var remainingKeys []string
|
var remainingKeys []string
|
||||||
|
|||||||
@ -14,7 +14,30 @@ type GeminiChatRequest struct {
|
|||||||
SafetySettings []GeminiChatSafetySettings `json:"safetySettings,omitempty"`
|
SafetySettings []GeminiChatSafetySettings `json:"safetySettings,omitempty"`
|
||||||
GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"`
|
GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"`
|
||||||
Tools json.RawMessage `json:"tools,omitempty"`
|
Tools json.RawMessage `json:"tools,omitempty"`
|
||||||
|
ToolConfig *ToolConfig `json:"toolConfig,omitempty"`
|
||||||
SystemInstructions *GeminiChatContent `json:"systemInstruction,omitempty"`
|
SystemInstructions *GeminiChatContent `json:"systemInstruction,omitempty"`
|
||||||
|
CachedContent string `json:"cachedContent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolConfig struct {
|
||||||
|
FunctionCallingConfig *FunctionCallingConfig `json:"functionCallingConfig,omitempty"`
|
||||||
|
RetrievalConfig *RetrievalConfig `json:"retrievalConfig,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FunctionCallingConfig struct {
|
||||||
|
Mode FunctionCallingConfigMode `json:"mode,omitempty"`
|
||||||
|
AllowedFunctionNames []string `json:"allowedFunctionNames,omitempty"`
|
||||||
|
}
|
||||||
|
type FunctionCallingConfigMode string
|
||||||
|
|
||||||
|
type RetrievalConfig struct {
|
||||||
|
LatLng *LatLng `json:"latLng,omitempty"`
|
||||||
|
LanguageCode string `json:"languageCode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LatLng struct {
|
||||||
|
Latitude *float64 `json:"latitude,omitempty"`
|
||||||
|
Longitude *float64 `json:"longitude,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *GeminiChatRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
func (r *GeminiChatRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||||
@ -239,12 +262,20 @@ type GeminiChatGenerationConfig struct {
|
|||||||
StopSequences []string `json:"stopSequences,omitempty"`
|
StopSequences []string `json:"stopSequences,omitempty"`
|
||||||
ResponseMimeType string `json:"responseMimeType,omitempty"`
|
ResponseMimeType string `json:"responseMimeType,omitempty"`
|
||||||
ResponseSchema any `json:"responseSchema,omitempty"`
|
ResponseSchema any `json:"responseSchema,omitempty"`
|
||||||
|
ResponseJsonSchema json.RawMessage `json:"responseJsonSchema,omitempty"`
|
||||||
|
PresencePenalty *float32 `json:"presencePenalty,omitempty"`
|
||||||
|
FrequencyPenalty *float32 `json:"frequencyPenalty,omitempty"`
|
||||||
|
ResponseLogprobs bool `json:"responseLogprobs,omitempty"`
|
||||||
|
Logprobs *int32 `json:"logprobs,omitempty"`
|
||||||
|
MediaResolution MediaResolution `json:"mediaResolution,omitempty"`
|
||||||
Seed int64 `json:"seed,omitempty"`
|
Seed int64 `json:"seed,omitempty"`
|
||||||
ResponseModalities []string `json:"responseModalities,omitempty"`
|
ResponseModalities []string `json:"responseModalities,omitempty"`
|
||||||
ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"`
|
ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"`
|
||||||
SpeechConfig json.RawMessage `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config
|
SpeechConfig json.RawMessage `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MediaResolution string
|
||||||
|
|
||||||
type GeminiChatCandidate struct {
|
type GeminiChatCandidate struct {
|
||||||
Content GeminiChatContent `json:"content"`
|
Content GeminiChatContent `json:"content"`
|
||||||
FinishReason *string `json:"finishReason"`
|
FinishReason *string `json:"finishReason"`
|
||||||
|
|||||||
@ -772,11 +772,12 @@ type OpenAIResponsesRequest struct {
|
|||||||
Instructions json.RawMessage `json:"instructions,omitempty"`
|
Instructions json.RawMessage `json:"instructions,omitempty"`
|
||||||
MaxOutputTokens uint `json:"max_output_tokens,omitempty"`
|
MaxOutputTokens uint `json:"max_output_tokens,omitempty"`
|
||||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||||
ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"`
|
ParallelToolCalls json.RawMessage `json:"parallel_tool_calls,omitempty"`
|
||||||
PreviousResponseID string `json:"previous_response_id,omitempty"`
|
PreviousResponseID string `json:"previous_response_id,omitempty"`
|
||||||
Reasoning *Reasoning `json:"reasoning,omitempty"`
|
Reasoning *Reasoning `json:"reasoning,omitempty"`
|
||||||
ServiceTier string `json:"service_tier,omitempty"`
|
ServiceTier string `json:"service_tier,omitempty"`
|
||||||
Store bool `json:"store,omitempty"`
|
Store json.RawMessage `json:"store,omitempty"`
|
||||||
|
PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
|
||||||
Stream bool `json:"stream,omitempty"`
|
Stream bool `json:"stream,omitempty"`
|
||||||
Temperature float64 `json:"temperature,omitempty"`
|
Temperature float64 `json:"temperature,omitempty"`
|
||||||
Text json.RawMessage `json:"text,omitempty"`
|
Text json.RawMessage `json:"text,omitempty"`
|
||||||
|
|||||||
6
main.go
6
main.go
@ -16,6 +16,7 @@ import (
|
|||||||
"one-api/setting/ratio_setting"
|
"one-api/setting/ratio_setting"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/gopkg/util/gopool"
|
"github.com/bytedance/gopkg/util/gopool"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
@ -33,6 +34,7 @@ var buildFS embed.FS
|
|||||||
var indexPage []byte
|
var indexPage []byte
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
err := InitResources()
|
err := InitResources()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -150,6 +152,10 @@ func main() {
|
|||||||
if port == "" {
|
if port == "" {
|
||||||
port = strconv.Itoa(*common.Port)
|
port = strconv.Itoa(*common.Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log startup success message
|
||||||
|
common.LogStartupSuccess(startTime, port)
|
||||||
|
|
||||||
err = server.Run(":" + port)
|
err = server.Run(":" + port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.FatalLog("failed to start HTTP server: " + err.Error())
|
common.FatalLog("failed to start HTTP server: " + err.Error())
|
||||||
|
|||||||
@ -21,6 +21,10 @@ var awsModelIDMap = map[string]string{
|
|||||||
"nova-lite-v1:0": "amazon.nova-lite-v1:0",
|
"nova-lite-v1:0": "amazon.nova-lite-v1:0",
|
||||||
"nova-pro-v1:0": "amazon.nova-pro-v1:0",
|
"nova-pro-v1:0": "amazon.nova-pro-v1:0",
|
||||||
"nova-premier-v1:0": "amazon.nova-premier-v1:0",
|
"nova-premier-v1:0": "amazon.nova-premier-v1:0",
|
||||||
|
"nova-canvas-v1:0": "amazon.nova-canvas-v1:0",
|
||||||
|
"nova-reel-v1:0": "amazon.nova-reel-v1:0",
|
||||||
|
"nova-reel-v1:1": "amazon.nova-reel-v1:1",
|
||||||
|
"nova-sonic-v1:0": "amazon.nova-sonic-v1:0",
|
||||||
}
|
}
|
||||||
|
|
||||||
var awsModelCanCrossRegionMap = map[string]map[string]bool{
|
var awsModelCanCrossRegionMap = map[string]map[string]bool{
|
||||||
@ -82,10 +86,27 @@ var awsModelCanCrossRegionMap = map[string]map[string]bool{
|
|||||||
"apac": true,
|
"apac": true,
|
||||||
},
|
},
|
||||||
"amazon.nova-premier-v1:0": {
|
"amazon.nova-premier-v1:0": {
|
||||||
|
"us": true,
|
||||||
|
},
|
||||||
|
"amazon.nova-canvas-v1:0": {
|
||||||
"us": true,
|
"us": true,
|
||||||
"eu": true,
|
"eu": true,
|
||||||
"apac": true,
|
"apac": true,
|
||||||
}}
|
},
|
||||||
|
"amazon.nova-reel-v1:0": {
|
||||||
|
"us": true,
|
||||||
|
"eu": true,
|
||||||
|
"apac": true,
|
||||||
|
},
|
||||||
|
"amazon.nova-reel-v1:1": {
|
||||||
|
"us": true,
|
||||||
|
},
|
||||||
|
"amazon.nova-sonic-v1:0": {
|
||||||
|
"us": true,
|
||||||
|
"eu": true,
|
||||||
|
"apac": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
var awsRegionCrossModelPrefixMap = map[string]string{
|
var awsRegionCrossModelPrefixMap = map[string]string{
|
||||||
"us": "us",
|
"us": "us",
|
||||||
|
|||||||
@ -80,8 +80,7 @@ func (a *TaskAdaptor) Init(info *relaycommon.RelayInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError {
|
func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycommon.RelayInfo) *dto.TaskError {
|
||||||
// Use the unified validation method for TaskSubmitReq with image-based action determination
|
return relaycommon.ValidateBasicTaskRequest(c, info, constant.TaskActionGenerate)
|
||||||
return relaycommon.ValidateTaskRequestWithImageBinding(c, info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, _ *relaycommon.RelayInfo) (io.Reader, error) {
|
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, _ *relaycommon.RelayInfo) (io.Reader, error) {
|
||||||
@ -112,6 +111,10 @@ func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, erro
|
|||||||
switch info.Action {
|
switch info.Action {
|
||||||
case constant.TaskActionGenerate:
|
case constant.TaskActionGenerate:
|
||||||
path = "/img2video"
|
path = "/img2video"
|
||||||
|
case constant.TaskActionFirstTailGenerate:
|
||||||
|
path = "/start-end2video"
|
||||||
|
case constant.TaskActionReferenceGenerate:
|
||||||
|
path = "/reference2video"
|
||||||
default:
|
default:
|
||||||
path = "/text2video"
|
path = "/text2video"
|
||||||
}
|
}
|
||||||
@ -187,14 +190,9 @@ func (a *TaskAdaptor) GetChannelName() string {
|
|||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*requestPayload, error) {
|
func (a *TaskAdaptor) convertToRequestPayload(req *relaycommon.TaskSubmitReq) (*requestPayload, error) {
|
||||||
var images []string
|
|
||||||
if req.Image != "" {
|
|
||||||
images = []string{req.Image}
|
|
||||||
}
|
|
||||||
|
|
||||||
r := requestPayload{
|
r := requestPayload{
|
||||||
Model: defaultString(req.Model, "viduq1"),
|
Model: defaultString(req.Model, "viduq1"),
|
||||||
Images: images,
|
Images: req.Images,
|
||||||
Prompt: req.Prompt,
|
Prompt: req.Prompt,
|
||||||
Duration: defaultInt(req.Duration, 5),
|
Duration: defaultInt(req.Duration, 5),
|
||||||
Resolution: defaultString(req.Size, "1080p"),
|
Resolution: defaultString(req.Size, "1080p"),
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
|
channelconstant "one-api/constant"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/relay/channel"
|
"one-api/relay/channel"
|
||||||
"one-api/relay/channel/openai"
|
"one-api/relay/channel/openai"
|
||||||
@ -188,20 +189,26 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||||
|
// 支持自定义域名,如果未设置则使用默认域名
|
||||||
|
baseUrl := info.ChannelBaseUrl
|
||||||
|
if baseUrl == "" {
|
||||||
|
baseUrl = channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine]
|
||||||
|
}
|
||||||
|
|
||||||
switch info.RelayMode {
|
switch info.RelayMode {
|
||||||
case constant.RelayModeChatCompletions:
|
case constant.RelayModeChatCompletions:
|
||||||
if strings.HasPrefix(info.UpstreamModelName, "bot") {
|
if strings.HasPrefix(info.UpstreamModelName, "bot") {
|
||||||
return fmt.Sprintf("%s/api/v3/bots/chat/completions", info.ChannelBaseUrl), nil
|
return fmt.Sprintf("%s/api/v3/bots/chat/completions", baseUrl), nil
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s/api/v3/chat/completions", info.ChannelBaseUrl), nil
|
return fmt.Sprintf("%s/api/v3/chat/completions", baseUrl), nil
|
||||||
case constant.RelayModeEmbeddings:
|
case constant.RelayModeEmbeddings:
|
||||||
return fmt.Sprintf("%s/api/v3/embeddings", info.ChannelBaseUrl), nil
|
return fmt.Sprintf("%s/api/v3/embeddings", baseUrl), nil
|
||||||
case constant.RelayModeImagesGenerations:
|
case constant.RelayModeImagesGenerations:
|
||||||
return fmt.Sprintf("%s/api/v3/images/generations", info.ChannelBaseUrl), nil
|
return fmt.Sprintf("%s/api/v3/images/generations", baseUrl), nil
|
||||||
case constant.RelayModeImagesEdits:
|
case constant.RelayModeImagesEdits:
|
||||||
return fmt.Sprintf("%s/api/v3/images/edits", info.ChannelBaseUrl), nil
|
return fmt.Sprintf("%s/api/v3/images/edits", baseUrl), nil
|
||||||
case constant.RelayModeRerank:
|
case constant.RelayModeRerank:
|
||||||
return fmt.Sprintf("%s/api/v3/rerank", info.ChannelBaseUrl), nil
|
return fmt.Sprintf("%s/api/v3/rerank", baseUrl), nil
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("unsupported relay mode: %d", info.RelayMode)
|
return "", fmt.Errorf("unsupported relay mode: %d", info.RelayMode)
|
||||||
|
|||||||
@ -9,6 +9,11 @@ var ModelList = []string{
|
|||||||
"Doubao-lite-4k",
|
"Doubao-lite-4k",
|
||||||
"Doubao-embedding",
|
"Doubao-embedding",
|
||||||
"doubao-seedream-4-0-250828",
|
"doubao-seedream-4-0-250828",
|
||||||
|
"seedream-4-0-250828",
|
||||||
|
"doubao-seedance-1-0-pro-250528",
|
||||||
|
"seedance-1-0-pro-250528",
|
||||||
|
"doubao-seed-1-6-thinking-250715",
|
||||||
|
"seed-1-6-thinking-250715",
|
||||||
}
|
}
|
||||||
|
|
||||||
var ChannelName = "volcengine"
|
var ChannelName = "volcengine"
|
||||||
|
|||||||
@ -207,10 +207,6 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
|
||||||
conn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
data := requestOpenAI2Xunfei(textRequest, appId, domain)
|
data := requestOpenAI2Xunfei(textRequest, appId, domain)
|
||||||
err = conn.WriteJSON(data)
|
err = conn.WriteJSON(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -220,6 +216,9 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap
|
|||||||
dataChan := make(chan XunfeiChatResponse)
|
dataChan := make(chan XunfeiChatResponse)
|
||||||
stopChan := make(chan bool)
|
stopChan := make(chan bool)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
for {
|
for {
|
||||||
_, msg, err := conn.ReadMessage()
|
_, msg, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/constant"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
relaycommon "one-api/relay/common"
|
relaycommon "one-api/relay/common"
|
||||||
"one-api/relay/helper"
|
"one-api/relay/helper"
|
||||||
@ -69,6 +70,31 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
|
|||||||
info.UpstreamModelName = request.Model
|
info.UpstreamModelName = request.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if info.ChannelSetting.SystemPrompt != "" {
|
||||||
|
if request.System == nil {
|
||||||
|
request.SetStringSystem(info.ChannelSetting.SystemPrompt)
|
||||||
|
} else if info.ChannelSetting.SystemPromptOverride {
|
||||||
|
common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true)
|
||||||
|
if request.IsStringSystem() {
|
||||||
|
existing := strings.TrimSpace(request.GetStringSystem())
|
||||||
|
if existing == "" {
|
||||||
|
request.SetStringSystem(info.ChannelSetting.SystemPrompt)
|
||||||
|
} else {
|
||||||
|
request.SetStringSystem(info.ChannelSetting.SystemPrompt + "\n" + existing)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
systemContents := request.ParseSystem()
|
||||||
|
newSystem := dto.ClaudeMediaMessage{Type: dto.ContentTypeText}
|
||||||
|
newSystem.SetText(info.ChannelSetting.SystemPrompt)
|
||||||
|
if len(systemContents) == 0 {
|
||||||
|
request.System = []dto.ClaudeMediaMessage{newSystem}
|
||||||
|
} else {
|
||||||
|
request.System = append([]dto.ClaudeMediaMessage{newSystem}, systemContents...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var requestBody io.Reader
|
var requestBody io.Reader
|
||||||
if model_setting.GetGlobalSettings().PassThroughRequestEnabled || info.ChannelSetting.PassThroughBodyEnabled {
|
if model_setting.GetGlobalSettings().PassThroughRequestEnabled || info.ChannelSetting.PassThroughBodyEnabled {
|
||||||
body, err := common.GetRequestBody(c)
|
body, err := common.GetRequestBody(c)
|
||||||
|
|||||||
@ -79,34 +79,18 @@ func ValidateBasicTaskRequest(c *gin.Context, info *RelayInfo, action string) *d
|
|||||||
req.Images = []string{req.Image}
|
req.Images = []string{req.Image}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.HasImage() {
|
||||||
|
action = constant.TaskActionGenerate
|
||||||
|
if info.ChannelType == constant.ChannelTypeVidu {
|
||||||
|
// vidu 增加 首尾帧生视频和参考图生视频
|
||||||
|
if len(req.Images) == 2 {
|
||||||
|
action = constant.TaskActionFirstTailGenerate
|
||||||
|
} else if len(req.Images) > 2 {
|
||||||
|
action = constant.TaskActionReferenceGenerate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
storeTaskRequest(c, info, action, req)
|
storeTaskRequest(c, info, action, req)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateTaskRequestWithImage(c *gin.Context, info *RelayInfo, requestObj interface{}) *dto.TaskError {
|
|
||||||
hasPrompt, ok := requestObj.(HasPrompt)
|
|
||||||
if !ok {
|
|
||||||
return createTaskError(fmt.Errorf("request must have prompt"), "invalid_request", http.StatusBadRequest, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if taskErr := validatePrompt(hasPrompt.GetPrompt()); taskErr != nil {
|
|
||||||
return taskErr
|
|
||||||
}
|
|
||||||
|
|
||||||
action := constant.TaskActionTextGenerate
|
|
||||||
if hasImage, ok := requestObj.(HasImage); ok && hasImage.HasImage() {
|
|
||||||
action = constant.TaskActionGenerate
|
|
||||||
}
|
|
||||||
|
|
||||||
storeTaskRequest(c, info, action, requestObj)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateTaskRequestWithImageBinding(c *gin.Context, info *RelayInfo) *dto.TaskError {
|
|
||||||
var req TaskSubmitReq
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
return createTaskError(err, "invalid_request_body", http.StatusBadRequest, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValidateTaskRequestWithImage(c, info, req)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/constant"
|
||||||
"one-api/dto"
|
"one-api/dto"
|
||||||
"one-api/logger"
|
"one-api/logger"
|
||||||
"one-api/relay/channel/gemini"
|
"one-api/relay/channel/gemini"
|
||||||
@ -94,6 +95,32 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
|
|||||||
|
|
||||||
adaptor.Init(info)
|
adaptor.Init(info)
|
||||||
|
|
||||||
|
if info.ChannelSetting.SystemPrompt != "" {
|
||||||
|
if request.SystemInstructions == nil {
|
||||||
|
request.SystemInstructions = &dto.GeminiChatContent{
|
||||||
|
Parts: []dto.GeminiPart{
|
||||||
|
{Text: info.ChannelSetting.SystemPrompt},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if len(request.SystemInstructions.Parts) == 0 {
|
||||||
|
request.SystemInstructions.Parts = []dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}
|
||||||
|
} else if info.ChannelSetting.SystemPromptOverride {
|
||||||
|
common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true)
|
||||||
|
merged := false
|
||||||
|
for i := range request.SystemInstructions.Parts {
|
||||||
|
if request.SystemInstructions.Parts[i].Text == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
request.SystemInstructions.Parts[i].Text = info.ChannelSetting.SystemPrompt + "\n" + request.SystemInstructions.Parts[i].Text
|
||||||
|
merged = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !merged {
|
||||||
|
request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}, request.SystemInstructions.Parts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up empty system instruction
|
// Clean up empty system instruction
|
||||||
if request.SystemInstructions != nil {
|
if request.SystemInstructions != nil {
|
||||||
hasContent := false
|
hasContent := false
|
||||||
|
|||||||
9
web/jsconfig.json
Normal file
9
web/jsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
@ -181,8 +181,8 @@ export function PreCode(props) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const code =
|
const codeElement = ref.current.querySelector('code');
|
||||||
ref.current.querySelector('code')?.innerText ?? '';
|
const code = codeElement?.textContent ?? '';
|
||||||
copy(code).then((success) => {
|
copy(code).then((success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
Toast.success(t('代码已复制到剪贴板'));
|
Toast.success(t('代码已复制到剪贴板'));
|
||||||
|
|||||||
@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Avatar, Button, Dropdown, Typography } from '@douyinfe/semi-ui';
|
import { Avatar, Button, Dropdown, Typography } from '@douyinfe/semi-ui';
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
@ -39,6 +39,7 @@ const UserArea = ({
|
|||||||
navigate,
|
navigate,
|
||||||
t,
|
t,
|
||||||
}) => {
|
}) => {
|
||||||
|
const dropdownRef = useRef(null);
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SkeletonWrapper
|
<SkeletonWrapper
|
||||||
@ -52,90 +53,93 @@ const UserArea = ({
|
|||||||
|
|
||||||
if (userState.user) {
|
if (userState.user) {
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<div className='relative' ref={dropdownRef}>
|
||||||
position='bottomRight'
|
<Dropdown
|
||||||
render={
|
position='bottomRight'
|
||||||
<Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
|
getPopupContainer={() => dropdownRef.current}
|
||||||
<Dropdown.Item
|
render={
|
||||||
onClick={() => {
|
<Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
|
||||||
navigate('/console/personal');
|
<Dropdown.Item
|
||||||
}}
|
onClick={() => {
|
||||||
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
|
navigate('/console/personal');
|
||||||
>
|
}}
|
||||||
<div className='flex items-center gap-2'>
|
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
|
||||||
<IconUserSetting
|
>
|
||||||
size='small'
|
<div className='flex items-center gap-2'>
|
||||||
className='text-gray-500 dark:text-gray-400'
|
<IconUserSetting
|
||||||
/>
|
size='small'
|
||||||
<span>{t('个人设置')}</span>
|
className='text-gray-500 dark:text-gray-400'
|
||||||
</div>
|
/>
|
||||||
</Dropdown.Item>
|
<span>{t('个人设置')}</span>
|
||||||
<Dropdown.Item
|
</div>
|
||||||
onClick={() => {
|
</Dropdown.Item>
|
||||||
navigate('/console/token');
|
<Dropdown.Item
|
||||||
}}
|
onClick={() => {
|
||||||
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
|
navigate('/console/token');
|
||||||
>
|
}}
|
||||||
<div className='flex items-center gap-2'>
|
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
|
||||||
<IconKey
|
>
|
||||||
size='small'
|
<div className='flex items-center gap-2'>
|
||||||
className='text-gray-500 dark:text-gray-400'
|
<IconKey
|
||||||
/>
|
size='small'
|
||||||
<span>{t('令牌管理')}</span>
|
className='text-gray-500 dark:text-gray-400'
|
||||||
</div>
|
/>
|
||||||
</Dropdown.Item>
|
<span>{t('令牌管理')}</span>
|
||||||
<Dropdown.Item
|
</div>
|
||||||
onClick={() => {
|
</Dropdown.Item>
|
||||||
navigate('/console/topup');
|
<Dropdown.Item
|
||||||
}}
|
onClick={() => {
|
||||||
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
|
navigate('/console/topup');
|
||||||
>
|
}}
|
||||||
<div className='flex items-center gap-2'>
|
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white'
|
||||||
<IconCreditCard
|
>
|
||||||
size='small'
|
<div className='flex items-center gap-2'>
|
||||||
className='text-gray-500 dark:text-gray-400'
|
<IconCreditCard
|
||||||
/>
|
size='small'
|
||||||
<span>{t('钱包管理')}</span>
|
className='text-gray-500 dark:text-gray-400'
|
||||||
</div>
|
/>
|
||||||
</Dropdown.Item>
|
<span>{t('钱包管理')}</span>
|
||||||
<Dropdown.Item
|
</div>
|
||||||
onClick={logout}
|
</Dropdown.Item>
|
||||||
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-red-500 dark:hover:!text-white'
|
<Dropdown.Item
|
||||||
>
|
onClick={logout}
|
||||||
<div className='flex items-center gap-2'>
|
className='!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-red-500 dark:hover:!text-white'
|
||||||
<IconExit
|
>
|
||||||
size='small'
|
<div className='flex items-center gap-2'>
|
||||||
className='text-gray-500 dark:text-gray-400'
|
<IconExit
|
||||||
/>
|
size='small'
|
||||||
<span>{t('退出')}</span>
|
className='text-gray-500 dark:text-gray-400'
|
||||||
</div>
|
/>
|
||||||
</Dropdown.Item>
|
<span>{t('退出')}</span>
|
||||||
</Dropdown.Menu>
|
</div>
|
||||||
}
|
</Dropdown.Item>
|
||||||
>
|
</Dropdown.Menu>
|
||||||
<Button
|
}
|
||||||
theme='borderless'
|
|
||||||
type='tertiary'
|
|
||||||
className='flex items-center gap-1.5 !p-1 !rounded-full hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'
|
|
||||||
>
|
>
|
||||||
<Avatar
|
<Button
|
||||||
size='extra-small'
|
theme='borderless'
|
||||||
color={stringToColor(userState.user.username)}
|
type='tertiary'
|
||||||
className='mr-1'
|
className='flex items-center gap-1.5 !p-1 !rounded-full hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'
|
||||||
>
|
>
|
||||||
{userState.user.username[0].toUpperCase()}
|
<Avatar
|
||||||
</Avatar>
|
size='extra-small'
|
||||||
<span className='hidden md:inline'>
|
color={stringToColor(userState.user.username)}
|
||||||
<Typography.Text className='!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1'>
|
className='mr-1'
|
||||||
{userState.user.username}
|
>
|
||||||
</Typography.Text>
|
{userState.user.username[0].toUpperCase()}
|
||||||
</span>
|
</Avatar>
|
||||||
<ChevronDown
|
<span className='hidden md:inline'>
|
||||||
size={14}
|
<Typography.Text className='!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1'>
|
||||||
className='text-xs text-semi-color-text-2 dark:text-gray-400'
|
{userState.user.username}
|
||||||
/>
|
</Typography.Text>
|
||||||
</Button>
|
</span>
|
||||||
</Dropdown>
|
<ChevronDown
|
||||||
|
size={14}
|
||||||
|
className='text-xs text-semi-color-text-2 dark:text-gray-400'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const showRegisterButton = !isSelfUseMode;
|
const showRegisterButton = !isSelfUseMode;
|
||||||
|
|||||||
@ -44,6 +44,7 @@ import CodeViewer from '../../../playground/CodeViewer';
|
|||||||
import { StatusContext } from '../../../../context/Status';
|
import { StatusContext } from '../../../../context/Status';
|
||||||
import { UserContext } from '../../../../context/User';
|
import { UserContext } from '../../../../context/User';
|
||||||
import { useUserPermissions } from '../../../../hooks/common/useUserPermissions';
|
import { useUserPermissions } from '../../../../hooks/common/useUserPermissions';
|
||||||
|
import { useSidebar } from '../../../../hooks/common/useSidebar';
|
||||||
|
|
||||||
const NotificationSettings = ({
|
const NotificationSettings = ({
|
||||||
t,
|
t,
|
||||||
@ -97,6 +98,9 @@ const NotificationSettings = ({
|
|||||||
isSidebarModuleAllowed,
|
isSidebarModuleAllowed,
|
||||||
} = useUserPermissions();
|
} = useUserPermissions();
|
||||||
|
|
||||||
|
// 使用useSidebar钩子获取刷新方法
|
||||||
|
const { refreshUserConfig } = useSidebar();
|
||||||
|
|
||||||
// 左侧边栏设置处理函数
|
// 左侧边栏设置处理函数
|
||||||
const handleSectionChange = (sectionKey) => {
|
const handleSectionChange = (sectionKey) => {
|
||||||
return (checked) => {
|
return (checked) => {
|
||||||
@ -132,6 +136,9 @@ const NotificationSettings = ({
|
|||||||
});
|
});
|
||||||
if (res.data.success) {
|
if (res.data.success) {
|
||||||
showSuccess(t('侧边栏设置保存成功'));
|
showSuccess(t('侧边栏设置保存成功'));
|
||||||
|
|
||||||
|
// 刷新useSidebar钩子中的用户配置,实现实时更新
|
||||||
|
await refreshUserConfig();
|
||||||
} else {
|
} else {
|
||||||
showError(res.data.message);
|
showError(res.data.message);
|
||||||
}
|
}
|
||||||
@ -334,7 +341,7 @@ const NotificationSettings = ({
|
|||||||
loading={sidebarLoading}
|
loading={sidebarLoading}
|
||||||
className='!rounded-lg'
|
className='!rounded-lg'
|
||||||
>
|
>
|
||||||
{t('保存边栏设置')}
|
{t('保存设置')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -87,6 +87,26 @@ const REGION_EXAMPLE = {
|
|||||||
'claude-3-5-sonnet-20240620': 'europe-west1',
|
'claude-3-5-sonnet-20240620': 'europe-west1',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 支持并且已适配通过接口获取模型列表的渠道类型
|
||||||
|
const MODEL_FETCHABLE_TYPES = new Set([
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
14,
|
||||||
|
34,
|
||||||
|
17,
|
||||||
|
26,
|
||||||
|
24,
|
||||||
|
47,
|
||||||
|
25,
|
||||||
|
20,
|
||||||
|
23,
|
||||||
|
31,
|
||||||
|
35,
|
||||||
|
40,
|
||||||
|
42,
|
||||||
|
48,
|
||||||
|
]);
|
||||||
|
|
||||||
function type2secretPrompt(type) {
|
function type2secretPrompt(type) {
|
||||||
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
|
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -260,7 +280,7 @@ const EditChannelModal = (props) => {
|
|||||||
pass_through_body_enabled: false,
|
pass_through_body_enabled: false,
|
||||||
system_prompt: '',
|
system_prompt: '',
|
||||||
});
|
});
|
||||||
const showApiConfigCard = inputs.type !== 45; // 控制是否显示 API 配置卡片(仅当渠道类型不是 豆包 时显示)
|
const showApiConfigCard = true; // 控制是否显示 API 配置卡片
|
||||||
const getInitValues = () => ({ ...originInputs });
|
const getInitValues = () => ({ ...originInputs });
|
||||||
|
|
||||||
// 处理渠道额外设置的更新
|
// 处理渠道额外设置的更新
|
||||||
@ -367,6 +387,10 @@ const EditChannelModal = (props) => {
|
|||||||
case 36:
|
case 36:
|
||||||
localModels = ['suno_music', 'suno_lyrics'];
|
localModels = ['suno_music', 'suno_lyrics'];
|
||||||
break;
|
break;
|
||||||
|
case 45:
|
||||||
|
localModels = getChannelModels(value);
|
||||||
|
setInputs((prevInputs) => ({ ...prevInputs, base_url: 'https://ark.cn-beijing.volces.com' }));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
localModels = getChannelModels(value);
|
localModels = getChannelModels(value);
|
||||||
break;
|
break;
|
||||||
@ -869,6 +893,10 @@ const EditChannelModal = (props) => {
|
|||||||
showInfo(t('请至少选择一个模型!'));
|
showInfo(t('请至少选择一个模型!'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (localInputs.type === 45 && (!localInputs.base_url || localInputs.base_url.trim() === '')) {
|
||||||
|
showInfo(t('请输入API地址!'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
localInputs.model_mapping &&
|
localInputs.model_mapping &&
|
||||||
localInputs.model_mapping !== '' &&
|
localInputs.model_mapping !== '' &&
|
||||||
@ -1876,6 +1904,30 @@ const EditChannelModal = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{inputs.type === 45 && (
|
||||||
|
<div>
|
||||||
|
<Form.Select
|
||||||
|
field='base_url'
|
||||||
|
label={t('API地址')}
|
||||||
|
placeholder={t('请选择API地址')}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleInputChange('base_url', value)
|
||||||
|
}
|
||||||
|
optionList={[
|
||||||
|
{
|
||||||
|
value: 'https://ark.cn-beijing.volces.com',
|
||||||
|
label: 'https://ark.cn-beijing.volces.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'https://ark.ap-southeast.bytepluses.com',
|
||||||
|
label: 'https://ark.ap-southeast.bytepluses.com'
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
defaultValue='https://ark.cn-beijing.volces.com'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -1961,13 +2013,15 @@ const EditChannelModal = (props) => {
|
|||||||
>
|
>
|
||||||
{t('填入所有模型')}
|
{t('填入所有模型')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{MODEL_FETCHABLE_TYPES.has(inputs.type) && (
|
||||||
size='small'
|
<Button
|
||||||
type='tertiary'
|
size='small'
|
||||||
onClick={() => fetchUpstreamModelList('models')}
|
type='tertiary'
|
||||||
>
|
onClick={() => fetchUpstreamModelList('models')}
|
||||||
{t('获取模型列表')}
|
>
|
||||||
</Button>
|
{t('获取模型列表')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
type='warning'
|
type='warning'
|
||||||
|
|||||||
@ -247,6 +247,32 @@ const MultiKeyManageModal = ({ visible, onCancel, channel, onRefresh }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Delete a specific key
|
||||||
|
const handleDeleteKey = async (keyIndex) => {
|
||||||
|
const operationId = `delete_${keyIndex}`;
|
||||||
|
setOperationLoading((prev) => ({ ...prev, [operationId]: true }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await API.post('/api/channel/multi_key/manage', {
|
||||||
|
channel_id: channel.id,
|
||||||
|
action: 'delete_key',
|
||||||
|
key_index: keyIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data.success) {
|
||||||
|
showSuccess(t('密钥已删除'));
|
||||||
|
await loadKeyStatus(currentPage, pageSize); // Reload current page
|
||||||
|
onRefresh && onRefresh(); // Refresh parent component
|
||||||
|
} else {
|
||||||
|
showError(res.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(t('删除密钥失败'));
|
||||||
|
} finally {
|
||||||
|
setOperationLoading((prev) => ({ ...prev, [operationId]: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle page change
|
// Handle page change
|
||||||
const handlePageChange = (page) => {
|
const handlePageChange = (page) => {
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
@ -384,7 +410,7 @@ const MultiKeyManageModal = ({ visible, onCancel, channel, onRefresh }) => {
|
|||||||
title: t('操作'),
|
title: t('操作'),
|
||||||
key: 'action',
|
key: 'action',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
width: 100,
|
width: 150,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space>
|
<Space>
|
||||||
{record.status === 1 ? (
|
{record.status === 1 ? (
|
||||||
@ -406,6 +432,21 @@ const MultiKeyManageModal = ({ visible, onCancel, channel, onRefresh }) => {
|
|||||||
{t('启用')}
|
{t('启用')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Popconfirm
|
||||||
|
title={t('确定要删除此密钥吗?')}
|
||||||
|
content={t('此操作不可撤销,将永久删除该密钥')}
|
||||||
|
onConfirm={() => handleDeleteKey(record.index)}
|
||||||
|
okType={'danger'}
|
||||||
|
position={'topRight'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type='danger'
|
||||||
|
size='small'
|
||||||
|
loading={operationLoading[`delete_${record.index}`]}
|
||||||
|
>
|
||||||
|
{t('删除')}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import React from 'react';
|
|||||||
import { Button, Form } from '@douyinfe/semi-ui';
|
import { Button, Form } from '@douyinfe/semi-ui';
|
||||||
import { IconSearch } from '@douyinfe/semi-icons';
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { DATE_RANGE_PRESETS } from '../../../constants/console.constants';
|
||||||
|
|
||||||
const MjLogsFilters = ({
|
const MjLogsFilters = ({
|
||||||
formInitValues,
|
formInitValues,
|
||||||
setFormApi,
|
setFormApi,
|
||||||
@ -54,6 +56,11 @@ const MjLogsFilters = ({
|
|||||||
showClear
|
showClear
|
||||||
pure
|
pure
|
||||||
size='small'
|
size='small'
|
||||||
|
presets={DATE_RANGE_PRESETS.map(preset => ({
|
||||||
|
text: t(preset.text),
|
||||||
|
start: preset.start(),
|
||||||
|
end: preset.end()
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -35,8 +35,9 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
TASK_ACTION_GENERATE,
|
TASK_ACTION_FIRST_TAIL_GENERATE,
|
||||||
TASK_ACTION_TEXT_GENERATE,
|
TASK_ACTION_GENERATE, TASK_ACTION_REFERENCE_GENERATE,
|
||||||
|
TASK_ACTION_TEXT_GENERATE
|
||||||
} from '../../../constants/common.constant';
|
} from '../../../constants/common.constant';
|
||||||
import { CHANNEL_OPTIONS } from '../../../constants/channel.constants';
|
import { CHANNEL_OPTIONS } from '../../../constants/channel.constants';
|
||||||
|
|
||||||
@ -111,6 +112,18 @@ const renderType = (type, t) => {
|
|||||||
{t('文生视频')}
|
{t('文生视频')}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
|
case TASK_ACTION_FIRST_TAIL_GENERATE:
|
||||||
|
return (
|
||||||
|
<Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
|
||||||
|
{t('首尾生视频')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
case TASK_ACTION_REFERENCE_GENERATE:
|
||||||
|
return (
|
||||||
|
<Tag color='blue' shape='circle' prefixIcon={<Sparkles size={14} />}>
|
||||||
|
{t('参照生视频')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
<Tag color='white' shape='circle' prefixIcon={<HelpCircle size={14} />}>
|
||||||
@ -343,7 +356,9 @@ export const getTaskLogsColumns = ({
|
|||||||
// 仅当为视频生成任务且成功,且 fail_reason 是 URL 时显示可点击链接
|
// 仅当为视频生成任务且成功,且 fail_reason 是 URL 时显示可点击链接
|
||||||
const isVideoTask =
|
const isVideoTask =
|
||||||
record.action === TASK_ACTION_GENERATE ||
|
record.action === TASK_ACTION_GENERATE ||
|
||||||
record.action === TASK_ACTION_TEXT_GENERATE;
|
record.action === TASK_ACTION_TEXT_GENERATE ||
|
||||||
|
record.action === TASK_ACTION_FIRST_TAIL_GENERATE ||
|
||||||
|
record.action === TASK_ACTION_REFERENCE_GENERATE;
|
||||||
const isSuccess = record.status === 'SUCCESS';
|
const isSuccess = record.status === 'SUCCESS';
|
||||||
const isUrl = typeof text === 'string' && /^https?:\/\//.test(text);
|
const isUrl = typeof text === 'string' && /^https?:\/\//.test(text);
|
||||||
if (isSuccess && isVideoTask && isUrl) {
|
if (isSuccess && isVideoTask && isUrl) {
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import React from 'react';
|
|||||||
import { Button, Form } from '@douyinfe/semi-ui';
|
import { Button, Form } from '@douyinfe/semi-ui';
|
||||||
import { IconSearch } from '@douyinfe/semi-icons';
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { DATE_RANGE_PRESETS } from '../../../constants/console.constants';
|
||||||
|
|
||||||
const TaskLogsFilters = ({
|
const TaskLogsFilters = ({
|
||||||
formInitValues,
|
formInitValues,
|
||||||
setFormApi,
|
setFormApi,
|
||||||
@ -54,6 +56,11 @@ const TaskLogsFilters = ({
|
|||||||
showClear
|
showClear
|
||||||
pure
|
pure
|
||||||
size='small'
|
size='small'
|
||||||
|
presets={DATE_RANGE_PRESETS.map(preset => ({
|
||||||
|
text: t(preset.text),
|
||||||
|
start: preset.start(),
|
||||||
|
end: preset.end()
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,8 @@ import React from 'react';
|
|||||||
import { Button, Form } from '@douyinfe/semi-ui';
|
import { Button, Form } from '@douyinfe/semi-ui';
|
||||||
import { IconSearch } from '@douyinfe/semi-icons';
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
|
|
||||||
|
import { DATE_RANGE_PRESETS } from '../../../constants/console.constants';
|
||||||
|
|
||||||
const LogsFilters = ({
|
const LogsFilters = ({
|
||||||
formInitValues,
|
formInitValues,
|
||||||
setFormApi,
|
setFormApi,
|
||||||
@ -55,6 +57,11 @@ const LogsFilters = ({
|
|||||||
showClear
|
showClear
|
||||||
pure
|
pure
|
||||||
size='small'
|
size='small'
|
||||||
|
presets={DATE_RANGE_PRESETS.map(preset => ({
|
||||||
|
text: t(preset.text),
|
||||||
|
start: preset.start(),
|
||||||
|
end: preset.end()
|
||||||
|
}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -40,3 +40,5 @@ export const API_ENDPOINTS = [
|
|||||||
|
|
||||||
export const TASK_ACTION_GENERATE = 'generate';
|
export const TASK_ACTION_GENERATE = 'generate';
|
||||||
export const TASK_ACTION_TEXT_GENERATE = 'textGenerate';
|
export const TASK_ACTION_TEXT_GENERATE = 'textGenerate';
|
||||||
|
export const TASK_ACTION_FIRST_TAIL_GENERATE = 'firstTailGenerate';
|
||||||
|
export const TASK_ACTION_REFERENCE_GENERATE = 'referenceGenerate';
|
||||||
|
|||||||
49
web/src/constants/console.constants.js
Normal file
49
web/src/constants/console.constants.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
// ========== 日期预设常量 ==========
|
||||||
|
export const DATE_RANGE_PRESETS = [
|
||||||
|
{
|
||||||
|
text: '今天',
|
||||||
|
start: () => dayjs().startOf('day').toDate(),
|
||||||
|
end: () => dayjs().endOf('day').toDate()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '近 7 天',
|
||||||
|
start: () => dayjs().subtract(6, 'day').startOf('day').toDate(),
|
||||||
|
end: () => dayjs().endOf('day').toDate()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '本周',
|
||||||
|
start: () => dayjs().startOf('week').toDate(),
|
||||||
|
end: () => dayjs().endOf('week').toDate()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '近 30 天',
|
||||||
|
start: () => dayjs().subtract(29, 'day').startOf('day').toDate(),
|
||||||
|
end: () => dayjs().endOf('day').toDate()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '本月',
|
||||||
|
start: () => dayjs().startOf('month').toDate(),
|
||||||
|
end: () => dayjs().endOf('month').toDate()
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -118,7 +118,6 @@ export const buildApiPayload = (
|
|||||||
model: inputs.model,
|
model: inputs.model,
|
||||||
group: inputs.group,
|
group: inputs.group,
|
||||||
messages: processedMessages,
|
messages: processedMessages,
|
||||||
group: inputs.group,
|
|
||||||
stream: inputs.stream,
|
stream: inputs.stream,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,13 +131,15 @@ export const buildApiPayload = (
|
|||||||
seed: 'seed',
|
seed: 'seed',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
Object.entries(parameterMappings).forEach(([key, param]) => {
|
Object.entries(parameterMappings).forEach(([key, param]) => {
|
||||||
if (
|
const enabled = parameterEnabled[key];
|
||||||
parameterEnabled[key] &&
|
const value = inputs[param];
|
||||||
inputs[param] !== undefined &&
|
const hasValue = value !== undefined && value !== null;
|
||||||
inputs[param] !== null
|
|
||||||
) {
|
|
||||||
payload[param] = inputs[param];
|
if (enabled && hasValue) {
|
||||||
|
payload[param] = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -75,13 +75,17 @@ export async function copy(text) {
|
|||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try {
|
try {
|
||||||
// 构建input 执行 复制命令
|
// 构建 textarea 执行复制命令,保留多行文本格式
|
||||||
var _input = window.document.createElement('input');
|
const textarea = window.document.createElement('textarea');
|
||||||
_input.value = text;
|
textarea.value = text;
|
||||||
window.document.body.appendChild(_input);
|
textarea.setAttribute('readonly', '');
|
||||||
_input.select();
|
textarea.style.position = 'fixed';
|
||||||
window.document.execCommand('Copy');
|
textarea.style.left = '-9999px';
|
||||||
window.document.body.removeChild(_input);
|
textarea.style.top = '-9999px';
|
||||||
|
window.document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
window.document.execCommand('copy');
|
||||||
|
window.document.body.removeChild(textarea);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
okay = false;
|
okay = false;
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@ -21,6 +21,10 @@ import { useState, useEffect, useMemo, useContext } from 'react';
|
|||||||
import { StatusContext } from '../../context/Status';
|
import { StatusContext } from '../../context/Status';
|
||||||
import { API } from '../../helpers';
|
import { API } from '../../helpers';
|
||||||
|
|
||||||
|
// 创建一个全局事件系统来同步所有useSidebar实例
|
||||||
|
const sidebarEventTarget = new EventTarget();
|
||||||
|
const SIDEBAR_REFRESH_EVENT = 'sidebar-refresh';
|
||||||
|
|
||||||
export const useSidebar = () => {
|
export const useSidebar = () => {
|
||||||
const [statusState] = useContext(StatusContext);
|
const [statusState] = useContext(StatusContext);
|
||||||
const [userConfig, setUserConfig] = useState(null);
|
const [userConfig, setUserConfig] = useState(null);
|
||||||
@ -124,9 +128,12 @@ export const useSidebar = () => {
|
|||||||
|
|
||||||
// 刷新用户配置的方法(供外部调用)
|
// 刷新用户配置的方法(供外部调用)
|
||||||
const refreshUserConfig = async () => {
|
const refreshUserConfig = async () => {
|
||||||
if (Object.keys(adminConfig).length > 0) {
|
if (Object.keys(adminConfig).length > 0) {
|
||||||
await loadUserConfig();
|
await loadUserConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发全局刷新事件,通知所有useSidebar实例更新
|
||||||
|
sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT));
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载用户配置
|
// 加载用户配置
|
||||||
@ -137,6 +144,21 @@ export const useSidebar = () => {
|
|||||||
}
|
}
|
||||||
}, [adminConfig]);
|
}, [adminConfig]);
|
||||||
|
|
||||||
|
// 监听全局刷新事件
|
||||||
|
useEffect(() => {
|
||||||
|
const handleRefresh = () => {
|
||||||
|
if (Object.keys(adminConfig).length > 0) {
|
||||||
|
loadUserConfig();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh);
|
||||||
|
};
|
||||||
|
}, [adminConfig]);
|
||||||
|
|
||||||
// 计算最终的显示配置
|
// 计算最终的显示配置
|
||||||
const finalConfig = useMemo(() => {
|
const finalConfig = useMemo(() => {
|
||||||
const result = {};
|
const result = {};
|
||||||
|
|||||||
@ -102,7 +102,7 @@ export const useDashboardStats = (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('统计Tokens'),
|
title: t('统计Tokens'),
|
||||||
value: isNaN(consumeTokens) ? 0 : consumeTokens,
|
value: isNaN(consumeTokens) ? 0 : consumeTokens.toLocaleString(),
|
||||||
icon: <IconTextStroked />,
|
icon: <IconTextStroked />,
|
||||||
avatarColor: 'pink',
|
avatarColor: 'pink',
|
||||||
trendData: trendData.tokens,
|
trendData: trendData.tokens,
|
||||||
|
|||||||
@ -1889,6 +1889,10 @@
|
|||||||
"确定要删除所有已自动禁用的密钥吗?": "Are you sure you want to delete all automatically disabled keys?",
|
"确定要删除所有已自动禁用的密钥吗?": "Are you sure you want to delete all automatically disabled keys?",
|
||||||
"此操作不可撤销,将永久删除已自动禁用的密钥": "This operation cannot be undone, and all automatically disabled keys will be permanently deleted.",
|
"此操作不可撤销,将永久删除已自动禁用的密钥": "This operation cannot be undone, and all automatically disabled keys will be permanently deleted.",
|
||||||
"删除自动禁用密钥": "Delete auto disabled keys",
|
"删除自动禁用密钥": "Delete auto disabled keys",
|
||||||
|
"确定要删除此密钥吗?": "Are you sure you want to delete this key?",
|
||||||
|
"此操作不可撤销,将永久删除该密钥": "This operation cannot be undone, and the key will be permanently deleted.",
|
||||||
|
"密钥已删除": "Key has been deleted",
|
||||||
|
"删除密钥失败": "Failed to delete key",
|
||||||
"图标": "Icon",
|
"图标": "Icon",
|
||||||
"模型图标": "Model icon",
|
"模型图标": "Model icon",
|
||||||
"请输入图标名称": "Please enter the icon name",
|
"请输入图标名称": "Please enter the icon name",
|
||||||
@ -2095,6 +2099,11 @@
|
|||||||
"优惠": "Discount",
|
"优惠": "Discount",
|
||||||
"折": "% off",
|
"折": "% off",
|
||||||
"节省": "Save",
|
"节省": "Save",
|
||||||
|
"今天": "Today",
|
||||||
|
"近 7 天": "Last 7 Days",
|
||||||
|
"本周": "This Week",
|
||||||
|
"本月": "This Month",
|
||||||
|
"近 30 天": "Last 30 Days",
|
||||||
"代理设置": "Proxy Settings",
|
"代理设置": "Proxy Settings",
|
||||||
"更新Worker设置": "Update Worker Settings",
|
"更新Worker设置": "Update Worker Settings",
|
||||||
"SSRF防护设置": "SSRF Protection Settings",
|
"SSRF防护设置": "SSRF Protection Settings",
|
||||||
|
|||||||
@ -20,10 +20,16 @@ For commercial licensing, please contact support@quantumnous.com
|
|||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { defineConfig, transformWithEsbuild } from 'vite';
|
import { defineConfig, transformWithEsbuild } from 'vite';
|
||||||
import pkg from '@douyinfe/vite-plugin-semi';
|
import pkg from '@douyinfe/vite-plugin-semi';
|
||||||
|
import path from 'path';
|
||||||
const { vitePluginSemi } = pkg;
|
const { vitePluginSemi } = pkg;
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
{
|
||||||
name: 'treat-js-files-as-jsx',
|
name: 'treat-js-files-as-jsx',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user