Commit 6fd746a3 by Calcium-Ion Committed by GitHub

Merge pull request #1956 from QuantumNous/alpha

alpha -> main
parents 621ee41f 76ce614f
...@@ -1102,6 +1102,9 @@ type UpdateUserSettingRequest struct { ...@@ -1102,6 +1102,9 @@ type UpdateUserSettingRequest struct {
WebhookSecret string `json:"webhook_secret,omitempty"` WebhookSecret string `json:"webhook_secret,omitempty"`
NotificationEmail string `json:"notification_email,omitempty"` NotificationEmail string `json:"notification_email,omitempty"`
BarkUrl string `json:"bark_url,omitempty"` BarkUrl string `json:"bark_url,omitempty"`
GotifyUrl string `json:"gotify_url,omitempty"`
GotifyToken string `json:"gotify_token,omitempty"`
GotifyPriority int `json:"gotify_priority,omitempty"`
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"` AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
RecordIpLog bool `json:"record_ip_log"` RecordIpLog bool `json:"record_ip_log"`
} }
...@@ -1117,7 +1120,7 @@ func UpdateUserSetting(c *gin.Context) { ...@@ -1117,7 +1120,7 @@ func UpdateUserSetting(c *gin.Context) {
} }
// 验证预警类型 // 验证预警类型
if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook && req.QuotaWarningType != dto.NotifyTypeBark { if req.QuotaWarningType != dto.NotifyTypeEmail && req.QuotaWarningType != dto.NotifyTypeWebhook && req.QuotaWarningType != dto.NotifyTypeBark && req.QuotaWarningType != dto.NotifyTypeGotify {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "无效的预警类型", "message": "无效的预警类型",
...@@ -1192,6 +1195,40 @@ func UpdateUserSetting(c *gin.Context) { ...@@ -1192,6 +1195,40 @@ func UpdateUserSetting(c *gin.Context) {
} }
} }
// 如果是Gotify类型,验证Gotify URL和Token
if req.QuotaWarningType == dto.NotifyTypeGotify {
if req.GotifyUrl == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "Gotify服务器地址不能为空",
})
return
}
if req.GotifyToken == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "Gotify令牌不能为空",
})
return
}
// 验证URL格式
if _, err := url.ParseRequestURI(req.GotifyUrl); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的Gotify服务器地址",
})
return
}
// 检查是否是HTTP或HTTPS
if !strings.HasPrefix(req.GotifyUrl, "https://") && !strings.HasPrefix(req.GotifyUrl, "http://") {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "Gotify服务器地址必须以http://或https://开头",
})
return
}
}
userId := c.GetInt("id") userId := c.GetInt("id")
user, err := model.GetUserById(userId, true) user, err := model.GetUserById(userId, true)
if err != nil { if err != nil {
...@@ -1225,6 +1262,18 @@ func UpdateUserSetting(c *gin.Context) { ...@@ -1225,6 +1262,18 @@ func UpdateUserSetting(c *gin.Context) {
settings.BarkUrl = req.BarkUrl settings.BarkUrl = req.BarkUrl
} }
// 如果是Gotify类型,添加Gotify配置到设置中
if req.QuotaWarningType == dto.NotifyTypeGotify {
settings.GotifyUrl = req.GotifyUrl
settings.GotifyToken = req.GotifyToken
// Gotify优先级范围0-10,超出范围则使用默认值5
if req.GotifyPriority < 0 || req.GotifyPriority > 10 {
settings.GotifyPriority = 5
} else {
settings.GotifyPriority = req.GotifyPriority
}
}
// 更新用户设置 // 更新用户设置
user.SetSetting(settings) user.SetSetting(settings)
if err := user.Update(false); err != nil { if err := user.Update(false); err != nil {
......
...@@ -20,6 +20,9 @@ type ChannelOtherSettings struct { ...@@ -20,6 +20,9 @@ type ChannelOtherSettings struct {
AzureResponsesVersion string `json:"azure_responses_version,omitempty"` AzureResponsesVersion string `json:"azure_responses_version,omitempty"`
VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key" VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"` OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"`
AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费)
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
} }
func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool { func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool {
......
...@@ -195,12 +195,15 @@ type ClaudeRequest struct { ...@@ -195,12 +195,15 @@ type ClaudeRequest struct {
Temperature *float64 `json:"temperature,omitempty"` Temperature *float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"` TopP float64 `json:"top_p,omitempty"`
TopK int `json:"top_k,omitempty"` TopK int `json:"top_k,omitempty"`
//ClaudeMetadata `json:"metadata,omitempty"`
Stream bool `json:"stream,omitempty"` Stream bool `json:"stream,omitempty"`
Tools any `json:"tools,omitempty"` Tools any `json:"tools,omitempty"`
ContextManagement json.RawMessage `json:"context_management,omitempty"` ContextManagement json.RawMessage `json:"context_management,omitempty"`
ToolChoice any `json:"tool_choice,omitempty"` ToolChoice any `json:"tool_choice,omitempty"`
Thinking *Thinking `json:"thinking,omitempty"` Thinking *Thinking `json:"thinking,omitempty"`
McpServers json.RawMessage `json:"mcp_servers,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
// 服务层级字段,用于指定 API 服务等级。允许透传可能导致实际计费高于预期,默认应过滤
ServiceTier string `json:"service_tier,omitempty"`
} }
func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta { func (c *ClaudeRequest) GetTokenCountMeta() *types.TokenCountMeta {
......
...@@ -57,6 +57,18 @@ type GeneralOpenAIRequest struct { ...@@ -57,6 +57,18 @@ type GeneralOpenAIRequest struct {
Dimensions int `json:"dimensions,omitempty"` Dimensions int `json:"dimensions,omitempty"`
Modalities json.RawMessage `json:"modalities,omitempty"` Modalities json.RawMessage `json:"modalities,omitempty"`
Audio json.RawMessage `json:"audio,omitempty"` Audio json.RawMessage `json:"audio,omitempty"`
// 安全标识符,用于帮助 OpenAI 检测可能违反使用政策的应用程序用户
// 注意:此字段会向 OpenAI 发送用户标识信息,默认过滤以保护用户隐私
SafetyIdentifier string `json:"safety_identifier,omitempty"`
// Whether or not to store the output of this chat completion request for use in our model distillation or evals products.
// 是否存储此次请求数据供 OpenAI 用于评估和优化产品
// 注意:默认过滤此字段以保护用户隐私,但过滤后可能导致 Codex 无法正常使用
Store json.RawMessage `json:"store,omitempty"`
// Used by OpenAI to cache responses for similar requests to optimize your cache hit rates. Replaces the user field
PromptCacheKey string `json:"prompt_cache_key,omitempty"`
LogitBias json.RawMessage `json:"logit_bias,omitempty"`
Metadata json.RawMessage `json:"metadata,omitempty"`
Prediction json.RawMessage `json:"prediction,omitempty"`
// gemini // gemini
ExtraBody json.RawMessage `json:"extra_body,omitempty"` ExtraBody json.RawMessage `json:"extra_body,omitempty"`
//xai //xai
...@@ -775,19 +787,20 @@ type OpenAIResponsesRequest struct { ...@@ -775,19 +787,20 @@ type OpenAIResponsesRequest struct {
ParallelToolCalls json.RawMessage `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"` // 服务层级字段,用于指定 API 服务等级。允许透传可能导致实际计费高于预期,默认应过滤
Store json.RawMessage `json:"store,omitempty"` ServiceTier string `json:"service_tier,omitempty"`
PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"` Store json.RawMessage `json:"store,omitempty"`
Stream bool `json:"stream,omitempty"` PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"`
Temperature float64 `json:"temperature,omitempty"` Stream bool `json:"stream,omitempty"`
Text json.RawMessage `json:"text,omitempty"` Temperature float64 `json:"temperature,omitempty"`
ToolChoice json.RawMessage `json:"tool_choice,omitempty"` Text json.RawMessage `json:"text,omitempty"`
Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
TopP float64 `json:"top_p,omitempty"` Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map
Truncation string `json:"truncation,omitempty"` TopP float64 `json:"top_p,omitempty"`
User string `json:"user,omitempty"` Truncation string `json:"truncation,omitempty"`
MaxToolCalls uint `json:"max_tool_calls,omitempty"` User string `json:"user,omitempty"`
Prompt json.RawMessage `json:"prompt,omitempty"` MaxToolCalls uint `json:"max_tool_calls,omitempty"`
Prompt json.RawMessage `json:"prompt,omitempty"`
} }
func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta { func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta {
......
...@@ -7,6 +7,9 @@ type UserSetting struct { ...@@ -7,6 +7,9 @@ type UserSetting struct {
WebhookSecret string `json:"webhook_secret,omitempty"` // WebhookSecret webhook密钥 WebhookSecret string `json:"webhook_secret,omitempty"` // WebhookSecret webhook密钥
NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址 NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址
BarkUrl string `json:"bark_url,omitempty"` // BarkUrl Bark推送URL BarkUrl string `json:"bark_url,omitempty"` // BarkUrl Bark推送URL
GotifyUrl string `json:"gotify_url,omitempty"` // GotifyUrl Gotify服务器地址
GotifyToken string `json:"gotify_token,omitempty"` // GotifyToken Gotify应用令牌
GotifyPriority int `json:"gotify_priority"` // GotifyPriority Gotify消息优先级
AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型 AcceptUnsetRatioModel bool `json:"accept_unset_model_ratio_model,omitempty"` // AcceptUnsetRatioModel 是否接受未设置价格的模型
RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP
SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置 SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置
...@@ -16,4 +19,5 @@ var ( ...@@ -16,4 +19,5 @@ var (
NotifyTypeEmail = "email" // Email 邮件 NotifyTypeEmail = "email" // Email 邮件
NotifyTypeWebhook = "webhook" // Webhook NotifyTypeWebhook = "webhook" // Webhook
NotifyTypeBark = "bark" // Bark 推送 NotifyTypeBark = "bark" // Bark 推送
NotifyTypeGotify = "gotify" // Gotify 推送
) )
...@@ -169,6 +169,9 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { ...@@ -169,6 +169,9 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
relayMode := relayconstant.RelayModeUnknown relayMode := relayconstant.RelayModeUnknown
if c.Request.Method == http.MethodPost { if c.Request.Method == http.MethodPost {
err = common.UnmarshalBodyReusable(c, &modelRequest) err = common.UnmarshalBodyReusable(c, &modelRequest)
if err != nil {
return nil, false, errors.New("video无效的请求, " + err.Error())
}
relayMode = relayconstant.RelayModeVideoSubmit relayMode = relayconstant.RelayModeVideoSubmit
} else if c.Request.Method == http.MethodGet { } else if c.Request.Method == http.MethodGet {
relayMode = relayconstant.RelayModeVideoFetchByID relayMode = relayconstant.RelayModeVideoFetchByID
......
...@@ -91,7 +91,43 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s ...@@ -91,7 +91,43 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s
} }
a.AccountCredentials = *adc a.AccountCredentials = *adc
if a.RequestMode == RequestModeLlama { if a.RequestMode == RequestModeGemini {
if region == "global" {
return fmt.Sprintf(
"https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/google/models/%s:%s",
adc.ProjectID,
modelName,
suffix,
), nil
} else {
return fmt.Sprintf(
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s",
region,
adc.ProjectID,
region,
modelName,
suffix,
), nil
}
} else if a.RequestMode == RequestModeClaude {
if region == "global" {
return fmt.Sprintf(
"https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/anthropic/models/%s:%s",
adc.ProjectID,
modelName,
suffix,
), nil
} else {
return fmt.Sprintf(
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/anthropic/models/%s:%s",
region,
adc.ProjectID,
region,
modelName,
suffix,
), nil
}
} else if a.RequestMode == RequestModeLlama {
return fmt.Sprintf( return fmt.Sprintf(
"https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions", "https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions",
region, region,
...@@ -99,42 +135,33 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s ...@@ -99,42 +135,33 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s
region, region,
), nil ), nil
} }
} else {
if region == "global" { var keyPrefix string
return fmt.Sprintf( if strings.HasSuffix(suffix, "?alt=sse") {
"https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/google/models/%s:%s", keyPrefix = "&"
adc.ProjectID,
modelName,
suffix,
), nil
} else { } else {
return fmt.Sprintf( keyPrefix = "?"
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:%s",
region,
adc.ProjectID,
region,
modelName,
suffix,
), nil
} }
} else {
if region == "global" { if region == "global" {
return fmt.Sprintf( return fmt.Sprintf(
"https://aiplatform.googleapis.com/v1/publishers/google/models/%s:%s?key=%s", "https://aiplatform.googleapis.com/v1/publishers/google/models/%s:%s%skey=%s",
modelName, modelName,
suffix, suffix,
keyPrefix,
info.ApiKey, info.ApiKey,
), nil ), nil
} else { } else {
return fmt.Sprintf( return fmt.Sprintf(
"https://%s-aiplatform.googleapis.com/v1/publishers/google/models/%s:%s?key=%s", "https://%s-aiplatform.googleapis.com/v1/publishers/google/models/%s:%s%skey=%s",
region, region,
modelName, modelName,
suffix, suffix,
keyPrefix,
info.ApiKey, info.ApiKey,
), nil ), nil
} }
} }
return "", errors.New("unsupported request mode")
} }
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
...@@ -188,7 +215,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel ...@@ -188,7 +215,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
} }
req.Set("Authorization", "Bearer "+accessToken) req.Set("Authorization", "Bearer "+accessToken)
} }
if a.AccountCredentials.ProjectID != "" { if a.AccountCredentials.ProjectID != "" {
req.Set("x-goog-user-project", a.AccountCredentials.ProjectID) req.Set("x-goog-user-project", a.AccountCredentials.ProjectID)
} }
return nil return nil
......
...@@ -112,6 +112,12 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ ...@@ -112,6 +112,12 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
} }
// remove disabled fields for Claude API
jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
}
// apply param override // apply param override
if len(info.ParamOverride) > 0 { if len(info.ParamOverride) > 0 {
jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride) jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride)
......
...@@ -509,3 +509,43 @@ type TaskInfo struct { ...@@ -509,3 +509,43 @@ type TaskInfo struct {
CompletionTokens int `json:"completion_tokens,omitempty"` // 用于按倍率计费 CompletionTokens int `json:"completion_tokens,omitempty"` // 用于按倍率计费
TotalTokens int `json:"total_tokens,omitempty"` // 用于按倍率计费 TotalTokens int `json:"total_tokens,omitempty"` // 用于按倍率计费
} }
// RemoveDisabledFields 从请求 JSON 数据中移除渠道设置中禁用的字段
// service_tier: 服务层级字段,可能导致额外计费(OpenAI、Claude、Responses API 支持)
// store: 数据存储授权字段,涉及用户隐私(仅 OpenAI、Responses API 支持,默认允许透传,禁用后可能导致 Codex 无法使用)
// safety_identifier: 安全标识符,用于向 OpenAI 报告违规用户(仅 OpenAI 支持,涉及用户隐私)
func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOtherSettings) ([]byte, error) {
var data map[string]interface{}
if err := common.Unmarshal(jsonData, &data); err != nil {
common.SysError("RemoveDisabledFields Unmarshal error :" + err.Error())
return jsonData, nil
}
// 默认移除 service_tier,除非明确允许(避免额外计费风险)
if !channelOtherSettings.AllowServiceTier {
if _, exists := data["service_tier"]; exists {
delete(data, "service_tier")
}
}
// 默认允许 store 透传,除非明确禁用(禁用可能影响 Codex 使用)
if channelOtherSettings.DisableStore {
if _, exists := data["store"]; exists {
delete(data, "store")
}
}
// 默认移除 safety_identifier,除非明确允许(保护用户隐私,避免向 OpenAI 报告用户信息)
if !channelOtherSettings.AllowSafetyIdentifier {
if _, exists := data["safety_identifier"]; exists {
delete(data, "safety_identifier")
}
}
jsonDataAfter, err := common.Marshal(data)
if err != nil {
common.SysError("RemoveDisabledFields Marshal error :" + err.Error())
return jsonData, nil
}
return jsonDataAfter, nil
}
...@@ -135,6 +135,12 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types ...@@ -135,6 +135,12 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
return types.NewError(err, types.ErrorCodeJsonMarshalFailed, types.ErrOptionWithSkipRetry()) return types.NewError(err, types.ErrorCodeJsonMarshalFailed, types.ErrOptionWithSkipRetry())
} }
// remove disabled fields for OpenAI API
jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
}
// apply param override // apply param override
if len(info.ParamOverride) > 0 { if len(info.ParamOverride) > 0 {
jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride) jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride)
......
...@@ -56,6 +56,13 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError * ...@@ -56,6 +56,13 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
if err != nil { if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry()) return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
} }
// remove disabled fields for OpenAI Responses API
jsonData, err = relaycommon.RemoveDisabledFields(jsonData, info.ChannelOtherSettings)
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
}
// apply param override // apply param override
if len(info.ParamOverride) > 0 { if len(info.ParamOverride) > 0 {
jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride) jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride)
......
...@@ -549,8 +549,11 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon ...@@ -549,8 +549,11 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon
// Bark推送使用简短文本,不支持HTML // Bark推送使用简短文本,不支持HTML
content = "{{value}},剩余额度:{{value}},请及时充值" content = "{{value}},剩余额度:{{value}},请及时充值"
values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)} values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)}
} else if notifyType == dto.NotifyTypeGotify {
content = "{{value}},当前剩余额度为 {{value}},请及时充值。"
values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)}
} else { } else {
// 默认内容格式,适用于Email和Webhook // 默认内容格式,适用于Email和Webhook(支持HTML)
content = "{{value}},当前剩余额度为 {{value}},为了不影响您的使用,请及时充值。<br/>充值链接:<a href='{{value}}'>{{value}}</a>" content = "{{value}},当前剩余额度为 {{value}},为了不影响您的使用,请及时充值。<br/>充值链接:<a href='{{value}}'>{{value}}</a>"
values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota), topUpLink, topUpLink} values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota), topUpLink, topUpLink}
} }
......
package service package service
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
...@@ -37,13 +39,16 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data ...@@ -37,13 +39,16 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data
switch notifyType { switch notifyType {
case dto.NotifyTypeEmail: case dto.NotifyTypeEmail:
// check setting email // 优先使用设置中的通知邮箱,如果为空则使用用户的默认邮箱
userEmail = userSetting.NotificationEmail emailToUse := userSetting.NotificationEmail
if userEmail == "" { if emailToUse == "" {
emailToUse = userEmail
}
if emailToUse == "" {
common.SysLog(fmt.Sprintf("user %d has no email, skip sending email", userId)) common.SysLog(fmt.Sprintf("user %d has no email, skip sending email", userId))
return nil return nil
} }
return sendEmailNotify(userEmail, data) return sendEmailNotify(emailToUse, data)
case dto.NotifyTypeWebhook: case dto.NotifyTypeWebhook:
webhookURLStr := userSetting.WebhookUrl webhookURLStr := userSetting.WebhookUrl
if webhookURLStr == "" { if webhookURLStr == "" {
...@@ -61,6 +66,14 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data ...@@ -61,6 +66,14 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data
return nil return nil
} }
return sendBarkNotify(barkURL, data) return sendBarkNotify(barkURL, data)
case dto.NotifyTypeGotify:
gotifyUrl := userSetting.GotifyUrl
gotifyToken := userSetting.GotifyToken
if gotifyUrl == "" || gotifyToken == "" {
common.SysLog(fmt.Sprintf("user %d has no gotify url or token, skip sending gotify", userId))
return nil
}
return sendGotifyNotify(gotifyUrl, gotifyToken, userSetting.GotifyPriority, data)
} }
return nil return nil
} }
...@@ -144,3 +157,98 @@ func sendBarkNotify(barkURL string, data dto.Notify) error { ...@@ -144,3 +157,98 @@ func sendBarkNotify(barkURL string, data dto.Notify) error {
return nil return nil
} }
func sendGotifyNotify(gotifyUrl string, gotifyToken string, priority int, data dto.Notify) error {
// 处理占位符
content := data.Content
for _, value := range data.Values {
content = strings.Replace(content, dto.ContentValueParam, fmt.Sprintf("%v", value), 1)
}
// 构建完整的 Gotify API URL
// 确保 URL 以 /message 结尾
finalURL := strings.TrimSuffix(gotifyUrl, "/") + "/message?token=" + url.QueryEscape(gotifyToken)
// Gotify优先级范围0-10,如果超出范围则使用默认值5
if priority < 0 || priority > 10 {
priority = 5
}
// 构建 JSON payload
type GotifyMessage struct {
Title string `json:"title"`
Message string `json:"message"`
Priority int `json:"priority"`
}
payload := GotifyMessage{
Title: data.Title,
Message: content,
Priority: priority,
}
// 序列化为 JSON
payloadBytes, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to marshal gotify payload: %v", err)
}
var req *http.Request
var resp *http.Response
if system_setting.EnableWorker() {
// 使用worker发送请求
workerReq := &WorkerRequest{
URL: finalURL,
Key: system_setting.WorkerValidKey,
Method: http.MethodPost,
Headers: map[string]string{
"Content-Type": "application/json; charset=utf-8",
"User-Agent": "OneAPI-Gotify-Notify/1.0",
},
Body: payloadBytes,
}
resp, err = DoWorkerRequest(workerReq)
if err != nil {
return fmt.Errorf("failed to send gotify request through worker: %v", err)
}
defer resp.Body.Close()
// 检查响应状态
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("gotify request failed with status code: %d", resp.StatusCode)
}
} else {
// SSRF防护:验证Gotify URL(非Worker模式)
fetchSetting := system_setting.GetFetchSetting()
if err := common.ValidateURLWithFetchSetting(finalURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
return fmt.Errorf("request reject: %v", err)
}
// 直接发送请求
req, err = http.NewRequest(http.MethodPost, finalURL, bytes.NewBuffer(payloadBytes))
if err != nil {
return fmt.Errorf("failed to create gotify request: %v", err)
}
// 设置请求头
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("User-Agent", "NewAPI-Gotify-Notify/1.0")
// 发送请求
client := GetHttpClient()
resp, err = client.Do(req)
if err != nil {
return fmt.Errorf("failed to send gotify request: %v", err)
}
defer resp.Body.Close()
// 检查响应状态
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("gotify request failed with status code: %d", resp.StatusCode)
}
}
return nil
}
...@@ -81,6 +81,9 @@ const PersonalSetting = () => { ...@@ -81,6 +81,9 @@ const PersonalSetting = () => {
webhookSecret: '', webhookSecret: '',
notificationEmail: '', notificationEmail: '',
barkUrl: '', barkUrl: '',
gotifyUrl: '',
gotifyToken: '',
gotifyPriority: 5,
acceptUnsetModelRatioModel: false, acceptUnsetModelRatioModel: false,
recordIpLog: false, recordIpLog: false,
}); });
...@@ -149,6 +152,12 @@ const PersonalSetting = () => { ...@@ -149,6 +152,12 @@ const PersonalSetting = () => {
webhookSecret: settings.webhook_secret || '', webhookSecret: settings.webhook_secret || '',
notificationEmail: settings.notification_email || '', notificationEmail: settings.notification_email || '',
barkUrl: settings.bark_url || '', barkUrl: settings.bark_url || '',
gotifyUrl: settings.gotify_url || '',
gotifyToken: settings.gotify_token || '',
gotifyPriority:
settings.gotify_priority !== undefined
? settings.gotify_priority
: 5,
acceptUnsetModelRatioModel: acceptUnsetModelRatioModel:
settings.accept_unset_model_ratio_model || false, settings.accept_unset_model_ratio_model || false,
recordIpLog: settings.record_ip_log || false, recordIpLog: settings.record_ip_log || false,
...@@ -406,6 +415,12 @@ const PersonalSetting = () => { ...@@ -406,6 +415,12 @@ const PersonalSetting = () => {
webhook_secret: notificationSettings.webhookSecret, webhook_secret: notificationSettings.webhookSecret,
notification_email: notificationSettings.notificationEmail, notification_email: notificationSettings.notificationEmail,
bark_url: notificationSettings.barkUrl, bark_url: notificationSettings.barkUrl,
gotify_url: notificationSettings.gotifyUrl,
gotify_token: notificationSettings.gotifyToken,
gotify_priority: (() => {
const parsed = parseInt(notificationSettings.gotifyPriority);
return isNaN(parsed) ? 5 : parsed;
})(),
accept_unset_model_ratio_model: accept_unset_model_ratio_model:
notificationSettings.acceptUnsetModelRatioModel, notificationSettings.acceptUnsetModelRatioModel,
record_ip_log: notificationSettings.recordIpLog, record_ip_log: notificationSettings.recordIpLog,
......
...@@ -400,6 +400,7 @@ const NotificationSettings = ({ ...@@ -400,6 +400,7 @@ const NotificationSettings = ({
<Radio value='email'>{t('邮件通知')}</Radio> <Radio value='email'>{t('邮件通知')}</Radio>
<Radio value='webhook'>{t('Webhook通知')}</Radio> <Radio value='webhook'>{t('Webhook通知')}</Radio>
<Radio value='bark'>{t('Bark通知')}</Radio> <Radio value='bark'>{t('Bark通知')}</Radio>
<Radio value='gotify'>{t('Gotify通知')}</Radio>
</Form.RadioGroup> </Form.RadioGroup>
<Form.AutoComplete <Form.AutoComplete
...@@ -589,7 +590,108 @@ const NotificationSettings = ({ ...@@ -589,7 +590,108 @@ const NotificationSettings = ({
rel='noopener noreferrer' rel='noopener noreferrer'
className='text-blue-500 hover:text-blue-600 font-medium' className='text-blue-500 hover:text-blue-600 font-medium'
> >
Bark 官方文档 Bark {t('官方文档')}
</a>
</div>
</div>
</div>
</>
)}
{/* Gotify推送设置 */}
{notificationSettings.warningType === 'gotify' && (
<>
<Form.Input
field='gotifyUrl'
label={t('Gotify服务器地址')}
placeholder={t(
'请输入Gotify服务器地址,例如: https://gotify.example.com',
)}
onChange={(val) => handleFormChange('gotifyUrl', val)}
prefix={<IconLink />}
extraText={t(
'支持HTTP和HTTPS,填写Gotify服务器的完整URL地址',
)}
showClear
rules={[
{
required:
notificationSettings.warningType === 'gotify',
message: t('请输入Gotify服务器地址'),
},
{
pattern: /^https?:\/\/.+/,
message: t('Gotify服务器地址必须以http://或https://开头'),
},
]}
/>
<Form.Input
field='gotifyToken'
label={t('Gotify应用令牌')}
placeholder={t('请输入Gotify应用令牌')}
onChange={(val) => handleFormChange('gotifyToken', val)}
prefix={<IconKey />}
extraText={t(
'在Gotify服务器创建应用后获得的令牌,用于发送通知',
)}
showClear
rules={[
{
required:
notificationSettings.warningType === 'gotify',
message: t('请输入Gotify应用令牌'),
},
]}
/>
<Form.AutoComplete
field='gotifyPriority'
label={t('消息优先级')}
placeholder={t('请选择消息优先级')}
data={[
{ value: 0, label: t('0 - 最低') },
{ value: 2, label: t('2 - 低') },
{ value: 5, label: t('5 - 正常(默认)') },
{ value: 8, label: t('8 - 高') },
{ value: 10, label: t('10 - 最高') },
]}
onChange={(val) =>
handleFormChange('gotifyPriority', val)
}
prefix={<IconBell />}
extraText={t('消息优先级,范围0-10,默认为5')}
style={{ width: '100%', maxWidth: '300px' }}
/>
<div className='mt-3 p-4 bg-gray-50/50 rounded-xl'>
<div className='text-sm text-gray-700 mb-3'>
<strong>{t('配置说明')}</strong>
</div>
<div className='text-xs text-gray-500 space-y-2'>
<div>
1. {t('在Gotify服务器的应用管理中创建新应用')}
</div>
<div>
2.{' '}
{t(
'复制应用的令牌(Token)并填写到上方的应用令牌字段',
)}
</div>
<div>
3. {t('填写Gotify服务器的完整URL地址')}
</div>
<div className='mt-3 pt-3 border-t border-gray-200'>
<span className='text-gray-400'>
{t('更多信息请参考')}
</span>{' '}
<a
href='https://gotify.net/'
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 hover:text-blue-600 font-medium'
>
Gotify {t('官方文档')}
</a> </a>
</div> </div>
</div> </div>
......
...@@ -1313,6 +1313,8 @@ ...@@ -1313,6 +1313,8 @@
"请输入Webhook地址,例如: https://example.com/webhook": "Please enter the Webhook URL, e.g.: https://example.com/webhook", "请输入Webhook地址,例如: https://example.com/webhook": "Please enter the Webhook URL, e.g.: https://example.com/webhook",
"邮件通知": "Email notification", "邮件通知": "Email notification",
"Webhook通知": "Webhook notification", "Webhook通知": "Webhook notification",
"Bark通知": "Bark notification",
"Gotify通知": "Gotify notification",
"接口凭证(可选)": "Interface credentials (optional)", "接口凭证(可选)": "Interface credentials (optional)",
"密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性": "The secret will be added to the request header as a Bearer token to verify the legitimacy of the webhook request", "密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性": "The secret will be added to the request header as a Bearer token to verify the legitimacy of the webhook request",
"Authorization: Bearer your-secret-key": "Authorization: Bearer your-secret-key", "Authorization: Bearer your-secret-key": "Authorization: Bearer your-secret-key",
...@@ -1323,6 +1325,36 @@ ...@@ -1323,6 +1325,36 @@
"通知邮箱": "Notification email", "通知邮箱": "Notification email",
"设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "Set the email address for receiving quota warning notifications, if not set, the email address bound to the account will be used", "设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "Set the email address for receiving quota warning notifications, if not set, the email address bound to the account will be used",
"留空则使用账号绑定的邮箱": "If left blank, the email address bound to the account will be used", "留空则使用账号绑定的邮箱": "If left blank, the email address bound to the account will be used",
"Bark推送URL": "Bark Push URL",
"请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}": "Please enter Bark push URL, e.g.: https://api.day.app/yourkey/{{title}}/{{content}}",
"支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)": "Supports HTTP and HTTPS, template variables: {{title}} (notification title), {{content}} (notification content)",
"请输入Bark推送URL": "Please enter Bark push URL",
"Bark推送URL必须以http://或https://开头": "Bark push URL must start with http:// or https://",
"模板示例": "Template example",
"更多参数请参考": "For more parameters, please refer to",
"Gotify服务器地址": "Gotify server address",
"请输入Gotify服务器地址,例如: https://gotify.example.com": "Please enter Gotify server address, e.g.: https://gotify.example.com",
"支持HTTP和HTTPS,填写Gotify服务器的完整URL地址": "Supports HTTP and HTTPS, enter the complete URL of the Gotify server",
"请输入Gotify服务器地址": "Please enter Gotify server address",
"Gotify服务器地址必须以http://或https://开头": "Gotify server address must start with http:// or https://",
"Gotify应用令牌": "Gotify application token",
"请输入Gotify应用令牌": "Please enter Gotify application token",
"在Gotify服务器创建应用后获得的令牌,用于发送通知": "Token obtained after creating an application on the Gotify server, used to send notifications",
"消息优先级": "Message priority",
"请选择消息优先级": "Please select message priority",
"0 - 最低": "0 - Lowest",
"2 - 低": "2 - Low",
"5 - 正常(默认)": "5 - Normal (default)",
"8 - 高": "8 - High",
"10 - 最高": "10 - Highest",
"消息优先级,范围0-10,默认为5": "Message priority, range 0-10, default is 5",
"配置说明": "Configuration instructions",
"在Gotify服务器的应用管理中创建新应用": "Create a new application in the Gotify server's application management",
"复制应用的令牌(Token)并填写到上方的应用令牌字段": "Copy the application token and fill it in the application token field above",
"填写Gotify服务器的完整URL地址": "Fill in the complete URL address of the Gotify server",
"更多信息请参考": "For more information, please refer to",
"通知内容": "Notification content",
"官方文档": "Official documentation",
"API地址": "Base URL", "API地址": "Base URL",
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in", "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in",
"渠道额外设置": "Channel extra settings", "渠道额外设置": "Channel extra settings",
...@@ -2191,6 +2223,13 @@ ...@@ -2191,6 +2223,13 @@
"输入 Origin 后回车,如:https://example.com": "Enter Origin and press Enter, e.g.: https://example.com", "输入 Origin 后回车,如:https://example.com": "Enter Origin and press Enter, e.g.: https://example.com",
"保存 Passkey 设置": "Save Passkey Settings", "保存 Passkey 设置": "Save Passkey Settings",
"黑名单": "Blacklist", "黑名单": "Blacklist",
"字段透传控制": "Field Pass-through Control",
"允许 service_tier 透传": "Allow service_tier Pass-through",
"禁用 store 透传": "Disable store Pass-through",
"允许 safety_identifier 透传": "Allow safety_identifier Pass-through",
"service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "The service_tier field is used to specify service level. Allowing pass-through may result in higher billing than expected. Disabled by default to avoid extra charges",
"store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "The store field authorizes OpenAI to store request data for product evaluation and optimization. Disabled by default. Enabling may cause Codex to malfunction",
"safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "The safety_identifier field helps OpenAI identify application users who may violate usage policies. Disabled by default to protect user privacy",
"common": { "common": {
"changeLanguage": "Change Language" "changeLanguage": "Change Language"
} }
......
...@@ -1308,6 +1308,8 @@ ...@@ -1308,6 +1308,8 @@
"请输入Webhook地址,例如: https://example.com/webhook": "Veuillez saisir l'URL du Webhook, par exemple : https://example.com/webhook", "请输入Webhook地址,例如: https://example.com/webhook": "Veuillez saisir l'URL du Webhook, par exemple : https://example.com/webhook",
"邮件通知": "Notification par e-mail", "邮件通知": "Notification par e-mail",
"Webhook通知": "Notification par Webhook", "Webhook通知": "Notification par Webhook",
"Bark通知": "Notification Bark",
"Gotify通知": "Notification Gotify",
"接口凭证(可选)": "Informations d'identification de l'interface (facultatif)", "接口凭证(可选)": "Informations d'identification de l'interface (facultatif)",
"密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性": "Le secret sera ajouté à l'en-tête de la requête en tant que jeton Bearer pour vérifier la légitimité de la requête webhook", "密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性": "Le secret sera ajouté à l'en-tête de la requête en tant que jeton Bearer pour vérifier la légitimité de la requête webhook",
"Authorization: Bearer your-secret-key": "Autorisation : Bearer votre-clé-secrète", "Authorization: Bearer your-secret-key": "Autorisation : Bearer votre-clé-secrète",
...@@ -1318,6 +1320,36 @@ ...@@ -1318,6 +1320,36 @@
"通知邮箱": "E-mail de notification", "通知邮箱": "E-mail de notification",
"设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "Définissez l'adresse e-mail pour recevoir les notifications d'avertissement de quota, si elle n'est pas définie, l'adresse e-mail liée au compte sera utilisée", "设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "Définissez l'adresse e-mail pour recevoir les notifications d'avertissement de quota, si elle n'est pas définie, l'adresse e-mail liée au compte sera utilisée",
"留空则使用账号绑定的邮箱": "Si ce champ est laissé vide, l'adresse e-mail liée au compte sera utilisée", "留空则使用账号绑定的邮箱": "Si ce champ est laissé vide, l'adresse e-mail liée au compte sera utilisée",
"Bark推送URL": "URL de notification Bark",
"请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}": "Veuillez saisir l'URL de notification Bark, par exemple : https://api.day.app/yourkey/{{title}}/{{content}}",
"支持HTTP和HTTPS,模板变量: {{title}} (通知标题), {{content}} (通知内容)": "Prend en charge HTTP et HTTPS, variables de modèle : {{title}} (titre de la notification), {{content}} (contenu de la notification)",
"请输入Bark推送URL": "Veuillez saisir l'URL de notification Bark",
"Bark推送URL必须以http://或https://开头": "L'URL de notification Bark doit commencer par http:// ou https://",
"模板示例": "Exemple de modèle",
"更多参数请参考": "Pour plus de paramètres, veuillez vous référer à",
"Gotify服务器地址": "Adresse du serveur Gotify",
"请输入Gotify服务器地址,例如: https://gotify.example.com": "Veuillez saisir l'adresse du serveur Gotify, par exemple : https://gotify.example.com",
"支持HTTP和HTTPS,填写Gotify服务器的完整URL地址": "Prend en charge HTTP et HTTPS, saisissez l'URL complète du serveur Gotify",
"请输入Gotify服务器地址": "Veuillez saisir l'adresse du serveur Gotify",
"Gotify服务器地址必须以http://或https://开头": "L'adresse du serveur Gotify doit commencer par http:// ou https://",
"Gotify应用令牌": "Jeton d'application Gotify",
"请输入Gotify应用令牌": "Veuillez saisir le jeton d'application Gotify",
"在Gotify服务器创建应用后获得的令牌,用于发送通知": "Jeton obtenu après la création d'une application sur le serveur Gotify, utilisé pour envoyer des notifications",
"消息优先级": "Priorité du message",
"请选择消息优先级": "Veuillez sélectionner la priorité du message",
"0 - 最低": "0 - La plus basse",
"2 - 低": "2 - Basse",
"5 - 正常(默认)": "5 - Normale (par défaut)",
"8 - 高": "8 - Haute",
"10 - 最高": "10 - La plus haute",
"消息优先级,范围0-10,默认为5": "Priorité du message, plage 0-10, par défaut 5",
"配置说明": "Instructions de configuration",
"在Gotify服务器的应用管理中创建新应用": "Créer une nouvelle application dans la gestion des applications du serveur Gotify",
"复制应用的令牌(Token)并填写到上方的应用令牌字段": "Copier le jeton de l'application et le remplir dans le champ de jeton d'application ci-dessus",
"填写Gotify服务器的完整URL地址": "Remplir l'adresse URL complète du serveur Gotify",
"更多信息请参考": "Pour plus d'informations, veuillez vous référer à",
"通知内容": "Contenu de la notification",
"官方文档": "Documentation officielle",
"API地址": "URL de base", "API地址": "URL de base",
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "Pour les canaux officiels, le new-api a une adresse intégrée. Sauf s'il s'agit d'un site proxy tiers ou d'une adresse d'accès Azure spéciale, il n'est pas nécessaire de la remplir", "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "Pour les canaux officiels, le new-api a une adresse intégrée. Sauf s'il s'agit d'un site proxy tiers ou d'une adresse d'accès Azure spéciale, il n'est pas nécessaire de la remplir",
"渠道额外设置": "Paramètres supplémentaires du canal", "渠道额外设置": "Paramètres supplémentaires du canal",
...@@ -2135,6 +2167,13 @@ ...@@ -2135,6 +2167,13 @@
"关闭侧边栏": "Fermer la barre latérale", "关闭侧边栏": "Fermer la barre latérale",
"定价": "Tarification", "定价": "Tarification",
"语言": "Langue", "语言": "Langue",
"字段透传控制": "Contrôle du passage des champs",
"允许 service_tier 透传": "Autoriser le passage de service_tier",
"禁用 store 透传": "Désactiver le passage de store",
"允许 safety_identifier 透传": "Autoriser le passage de safety_identifier",
"service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "Le champ service_tier est utilisé pour spécifier le niveau de service. Permettre le passage peut entraîner une facturation plus élevée que prévu. Désactivé par défaut pour éviter des frais supplémentaires",
"store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "Le champ store autorise OpenAI à stocker les données de requête pour l'évaluation et l'optimisation du produit. Désactivé par défaut. L'activation peut causer un dysfonctionnement de Codex",
"safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "Le champ safety_identifier aide OpenAI à identifier les utilisateurs d'applications susceptibles de violer les politiques d'utilisation. Désactivé par défaut pour protéger la confidentialité des utilisateurs",
"common": { "common": {
"changeLanguage": "Changer de langue" "changeLanguage": "Changer de langue"
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment