Merge remote-tracking branch 'origin/main' into feature/volcengine-base-url
This commit is contained in:
commit
95cbab6b19
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
|
||||||
|
|||||||
@ -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"`
|
||||||
|
|||||||
8
main.go
8
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())
|
||||||
@ -204,4 +210,4 @@ func InitResources() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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('代码已复制到剪贴板'));
|
||||||
|
|||||||
@ -85,6 +85,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) {
|
||||||
@ -1904,13 +1924,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>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user