From 465830945b424af5cf8d5f2e8dfa624f55281e9c Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Wed, 10 Sep 2025 22:36:03 +0800 Subject: [PATCH 01/11] fix: get video task err when Content-Type=json --- middleware/distributor.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/middleware/distributor.go b/middleware/distributor.go index 1e6df872d..3d929df49 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -166,9 +166,12 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { c.Set("platform", string(constant.TaskPlatformSuno)) c.Set("relay_mode", relayMode) } else if strings.Contains(c.Request.URL.Path, "/v1/video/generations") { - err = common.UnmarshalBodyReusable(c, &modelRequest) relayMode := relayconstant.RelayModeUnknown if c.Request.Method == http.MethodPost { + err = common.UnmarshalBodyReusable(c, &modelRequest) + if err != nil { + return nil, false, errors.New("video无效的请求, " + err.Error()) + } relayMode = relayconstant.RelayModeVideoSubmit } else if c.Request.Method == http.MethodGet { relayMode = relayconstant.RelayModeVideoFetchByID From f2e9fd7afb91a5ab7c988401927b5f36e7a960c5 Mon Sep 17 00:00:00 2001 From: wzxjohn Date: Tue, 16 Sep 2025 17:18:32 +0800 Subject: [PATCH 02/11] fix(relay): wrong URL for claude model in GCP Vertex AI --- relay/channel/vertex/adaptor.go | 59 ++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go index a424cb1a4..6398b8f62 100644 --- a/relay/channel/vertex/adaptor.go +++ b/relay/channel/vertex/adaptor.go @@ -90,7 +90,43 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s } 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( "https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions", region, @@ -98,24 +134,6 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s region, ), nil } - - 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 region == "global" { return fmt.Sprintf( @@ -134,6 +152,7 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s ), nil } } + return "", errors.New("unsupported request mode") } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { @@ -187,7 +206,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel } req.Set("Authorization", "Bearer "+accessToken) } - if a.AccountCredentials.ProjectID != "" { + if a.AccountCredentials.ProjectID != "" { req.Set("x-goog-user-project", a.AccountCredentials.ProjectID) } return nil From 8d92ce38ed2be32295a69926f843023dabdc7ef0 Mon Sep 17 00:00:00 2001 From: wzxjohn Date: Fri, 19 Sep 2025 11:22:03 +0800 Subject: [PATCH 03/11] fix(relay): wrong key param while enable sse --- relay/channel/vertex/adaptor.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go index 6398b8f62..742366b13 100644 --- a/relay/channel/vertex/adaptor.go +++ b/relay/channel/vertex/adaptor.go @@ -135,19 +135,27 @@ func (a *Adaptor) getRequestUrl(info *relaycommon.RelayInfo, modelName, suffix s ), nil } } else { + var keyPrefix string + if strings.HasSuffix(suffix, "?alt=sse") { + keyPrefix = "&" + } else { + keyPrefix = "?" + } if region == "global" { 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, suffix, + keyPrefix, info.ApiKey, ), nil } else { 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, modelName, suffix, + keyPrefix, info.ApiKey, ), nil } From ec9903e6404ee6dfed5d3ae9e236802cd0445c3c Mon Sep 17 00:00:00 2001 From: JoeyLearnsToCode Date: Fri, 19 Sep 2025 18:09:26 +0800 Subject: [PATCH 04/11] feat: jump between section on channel edit page --- .../channels/modals/EditChannelModal.jsx | 138 +++++++++++++++--- 1 file changed, 116 insertions(+), 22 deletions(-) diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index c0a216246..07d4f3925 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -66,6 +66,8 @@ import { IconCode, IconGlobe, IconBolt, + IconChevronUp, + IconChevronDown, } from '@douyinfe/semi-icons'; const { Text, Title } = Typography; @@ -184,6 +186,18 @@ const EditChannelModal = (props) => { const [verifyCode, setVerifyCode] = useState(''); const [verifyLoading, setVerifyLoading] = useState(false); + // 表单块导航相关状态 + const formSectionRefs = useRef({ + basicInfo: null, + apiConfig: null, + modelConfig: null, + advancedSettings: null, + channelExtraSettings: null, + }); + const [currentSectionIndex, setCurrentSectionIndex] = useState(0); + const formSections = ['basicInfo', 'apiConfig', 'modelConfig', 'advancedSettings', 'channelExtraSettings']; + const formContainerRef = useRef(null); + // 2FA状态更新辅助函数 const updateTwoFAState = (updates) => { setTwoFAState((prev) => ({ ...prev, ...updates })); @@ -207,6 +221,37 @@ const EditChannelModal = (props) => { setVerifyLoading(false); }; + // 表单导航功能 + const scrollToSection = (sectionKey) => { + const sectionElement = formSectionRefs.current[sectionKey]; + if (sectionElement) { + sectionElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'nearest' + }); + } + }; + + const navigateToSection = (direction) => { + const availableSections = formSections.filter(section => { + if (section === 'apiConfig') { + return showApiConfigCard; + } + return true; + }); + + let newIndex; + if (direction === 'up') { + newIndex = currentSectionIndex > 0 ? currentSectionIndex - 1 : availableSections.length - 1; + } else { + newIndex = currentSectionIndex < availableSections.length - 1 ? currentSectionIndex + 1 : 0; + } + + setCurrentSectionIndex(newIndex); + scrollToSection(availableSections[newIndex]); + }; + // 渠道额外设置状态 const [channelSettings, setChannelSettings] = useState({ force_format: false, @@ -672,6 +717,8 @@ const EditChannelModal = (props) => { fetchModelGroups(); // 重置手动输入模式状态 setUseManualInput(false); + // 重置导航状态 + setCurrentSectionIndex(0); } else { // 统一的模态框关闭重置逻辑 resetModalState(); @@ -1108,7 +1155,41 @@ const EditChannelModal = (props) => { visible={props.visible} width={isMobile ? '100%' : 600} footer={ -
+
+
+
+ + + ), + }, + ]; + return ( -
(refForm.current = formAPI)} - style={{ marginBottom: 15 }} - > + - { - return verifyJSON(value); - }, - message: t('不是合法的 JSON 字符串'), - }, - ]} - onChange={(value) => - setInputs({ - ...inputs, - Chats: value, - }) - } - /> + + + +
+ + {t('编辑模式')}: + + { + const newMode = e.target.value; + setEditMode(newMode); + + // 确保模式切换时数据正确同步 + setTimeout(() => { + if (newMode === 'json' && refForm.current) { + refForm.current.setValues(inputs); + } + }, 100); + }} + > + {t('可视化编辑')} + {t('JSON编辑')} + +
+ + {editMode === 'visual' ? ( +
+ + + } + placeholder={t('搜索聊天应用名称')} + value={searchText} + onChange={(value) => setSearchText(value)} + style={{ width: 250 }} + showClear + /> + + + + t('共 {{total}} 项,当前显示 {{start}}-{{end}} 项', { + total, + start: range[0], + end: range[1], + }), + }} + /> + + ) : ( + (refForm.current = formAPI)} + > + { + return verifyJSON(value); + }, + message: t('不是合法的 JSON 字符串'), + }, + ]} + onChange={(value) => + setInputs({ + ...inputs, + Chats: value, + }) + } + /> + + )} - - - + + + + + + +
(modalFormRef.current = api)}> + + + + +
); } From 14283385467676ef10bbec65ec1fe145d0d73ee6 Mon Sep 17 00:00:00 2001 From: HynoR <20227709+HynoR@users.noreply.github.com> Date: Wed, 1 Oct 2025 18:40:02 +0800 Subject: [PATCH 06/11] feat: Enhance SettingsChats edit interface --- web/src/pages/Setting/Chat/SettingsChats.jsx | 69 +++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/web/src/pages/Setting/Chat/SettingsChats.jsx b/web/src/pages/Setting/Chat/SettingsChats.jsx index 368a66f5b..01591c782 100644 --- a/web/src/pages/Setting/Chat/SettingsChats.jsx +++ b/web/src/pages/Setting/Chat/SettingsChats.jsx @@ -36,6 +36,7 @@ import { IconEdit, IconDelete, IconSearch, + IconSaveStroked, } from '@douyinfe/semi-icons'; import { compareObjects, @@ -55,7 +56,7 @@ export default function SettingsChats(props) { }); const refForm = useRef(); const [inputsRow, setInputsRow] = useState(inputs); - const [editMode, setEditMode] = useState('json'); + const [editMode, setEditMode] = useState('visual'); const [chatConfigs, setChatConfigs] = useState([]); const [modalVisible, setModalVisible] = useState(false); const [editingConfig, setEditingConfig] = useState(null); @@ -167,7 +168,9 @@ export default function SettingsChats(props) { } setInputs(currentInputs); setInputsRow(structuredClone(currentInputs)); - refForm.current.setValues(currentInputs); + if (refForm.current) { + refForm.current.setValues(currentInputs); + } // 同步到可视化配置 const configs = jsonToConfigs(currentInputs.Chats || '[]'); @@ -220,6 +223,18 @@ export default function SettingsChats(props) { modalFormRef.current .validate() .then((values) => { + // 检查名称是否重复 + const isDuplicate = chatConfigs.some( + (config) => + config.name === values.name && + (!isEdit || config.id !== editingConfig.id) + ); + + if (isDuplicate) { + showError(t('聊天应用名称已存在,请使用其他名称')); + return; + } + if (isEdit) { const newConfigs = chatConfigs.map((config) => config.id === editingConfig.id @@ -263,6 +278,28 @@ export default function SettingsChats(props) { config.name.toLowerCase().includes(searchText.toLowerCase()), ); + const highlightKeywords = (text) => { + if (!text) return text; + + const parts = text.split(/(\{address\}|\{key\})/g); + return parts.map((part, index) => { + if (part === '{address}') { + return ( + + {part} + + ); + } else if (part === '{key}') { + return ( + + {part} + + ); + } + return part; + }); + }; + const columns = [ { title: t('聊天应用名称'), @@ -275,7 +312,9 @@ export default function SettingsChats(props) { dataIndex: 'url', key: 'url', render: (text) => ( -
{text}
+
+ {highlightKeywords(text)} +
), }, { @@ -351,6 +390,14 @@ export default function SettingsChats(props) { > {t('添加聊天配置')} + } placeholder={t('搜索聊天应用名称')} @@ -410,11 +457,17 @@ export default function SettingsChats(props) { )} - - - + {editMode === 'json' && ( + + + + )} Date: Wed, 1 Oct 2025 19:15:00 +0800 Subject: [PATCH 07/11] feat: add Gotify notification option for quota alerts --- controller/user.go | 51 +++++++- dto/user_settings.go | 4 + service/quota.go | 5 +- service/user_notify.go | 116 +++++++++++++++++- .../components/settings/PersonalSetting.jsx | 15 +++ .../personal/cards/NotificationSettings.jsx | 102 +++++++++++++++ 6 files changed, 287 insertions(+), 6 deletions(-) diff --git a/controller/user.go b/controller/user.go index c03afa322..33d4636b7 100644 --- a/controller/user.go +++ b/controller/user.go @@ -1102,6 +1102,9 @@ type UpdateUserSettingRequest struct { WebhookSecret string `json:"webhook_secret,omitempty"` NotificationEmail string `json:"notification_email,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"` RecordIpLog bool `json:"record_ip_log"` } @@ -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{ "success": false, "message": "无效的预警类型", @@ -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") user, err := model.GetUserById(userId, true) if err != nil { @@ -1225,6 +1262,18 @@ func UpdateUserSetting(c *gin.Context) { 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) if err := user.Update(false); err != nil { diff --git a/dto/user_settings.go b/dto/user_settings.go index 89dd926ef..16ce7b985 100644 --- a/dto/user_settings.go +++ b/dto/user_settings.go @@ -7,6 +7,9 @@ type UserSetting struct { WebhookSecret string `json:"webhook_secret,omitempty"` // WebhookSecret webhook密钥 NotificationEmail string `json:"notification_email,omitempty"` // NotificationEmail 通知邮箱地址 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 是否接受未设置价格的模型 RecordIpLog bool `json:"record_ip_log,omitempty"` // 是否记录请求和错误日志IP SidebarModules string `json:"sidebar_modules,omitempty"` // SidebarModules 左侧边栏模块配置 @@ -16,4 +19,5 @@ var ( NotifyTypeEmail = "email" // Email 邮件 NotifyTypeWebhook = "webhook" // Webhook NotifyTypeBark = "bark" // Bark 推送 + NotifyTypeGotify = "gotify" // Gotify 推送 ) diff --git a/service/quota.go b/service/quota.go index 12017e11e..43c4024ae 100644 --- a/service/quota.go +++ b/service/quota.go @@ -549,8 +549,11 @@ func checkAndSendQuotaNotify(relayInfo *relaycommon.RelayInfo, quota int, preCon // Bark推送使用简短文本,不支持HTML content = "{{value}},剩余额度:{{value}},请及时充值" values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)} + } else if notifyType == dto.NotifyTypeGotify { + content = "{{value}},当前剩余额度为 {{value}},请及时充值。" + values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)} } else { - // 默认内容格式,适用于Email和Webhook + // 默认内容格式,适用于Email和Webhook(支持HTML) content = "{{value}},当前剩余额度为 {{value}},为了不影响您的使用,请及时充值。
充值链接:{{value}}" values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota), topUpLink, topUpLink} } diff --git a/service/user_notify.go b/service/user_notify.go index fba12d9db..0f92e7d75 100644 --- a/service/user_notify.go +++ b/service/user_notify.go @@ -1,6 +1,8 @@ package service import ( + "bytes" + "encoding/json" "fmt" "net/http" "net/url" @@ -37,13 +39,16 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data switch notifyType { case dto.NotifyTypeEmail: - // check setting email - userEmail = userSetting.NotificationEmail - if userEmail == "" { + // 优先使用设置中的通知邮箱,如果为空则使用用户的默认邮箱 + emailToUse := userSetting.NotificationEmail + if emailToUse == "" { + emailToUse = userEmail + } + if emailToUse == "" { common.SysLog(fmt.Sprintf("user %d has no email, skip sending email", userId)) return nil } - return sendEmailNotify(userEmail, data) + return sendEmailNotify(emailToUse, data) case dto.NotifyTypeWebhook: webhookURLStr := userSetting.WebhookUrl if webhookURLStr == "" { @@ -61,6 +66,14 @@ func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data return nil } 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 } @@ -144,3 +157,98 @@ func sendBarkNotify(barkURL string, data dto.Notify) error { 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 +} diff --git a/web/src/components/settings/PersonalSetting.jsx b/web/src/components/settings/PersonalSetting.jsx index 01e7023ad..c9934604c 100644 --- a/web/src/components/settings/PersonalSetting.jsx +++ b/web/src/components/settings/PersonalSetting.jsx @@ -81,6 +81,9 @@ const PersonalSetting = () => { webhookSecret: '', notificationEmail: '', barkUrl: '', + gotifyUrl: '', + gotifyToken: '', + gotifyPriority: 5, acceptUnsetModelRatioModel: false, recordIpLog: false, }); @@ -149,6 +152,12 @@ const PersonalSetting = () => { webhookSecret: settings.webhook_secret || '', notificationEmail: settings.notification_email || '', barkUrl: settings.bark_url || '', + gotifyUrl: settings.gotify_url || '', + gotifyToken: settings.gotify_token || '', + gotifyPriority: + settings.gotify_priority !== undefined + ? settings.gotify_priority + : 5, acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false, recordIpLog: settings.record_ip_log || false, @@ -406,6 +415,12 @@ const PersonalSetting = () => { webhook_secret: notificationSettings.webhookSecret, notification_email: notificationSettings.notificationEmail, 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: notificationSettings.acceptUnsetModelRatioModel, record_ip_log: notificationSettings.recordIpLog, diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index aad612d2c..dc428f145 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -400,6 +400,7 @@ const NotificationSettings = ({ {t('邮件通知')} {t('Webhook通知')} {t('Bark通知')} + {t('Gotify通知')} )} + + {/* Gotify推送设置 */} + {notificationSettings.warningType === 'gotify' && ( + <> + handleFormChange('gotifyUrl', val)} + prefix={} + extraText={t( + '支持HTTP和HTTPS,填写Gotify服务器的完整URL地址', + )} + showClear + rules={[ + { + required: + notificationSettings.warningType === 'gotify', + message: t('请输入Gotify服务器地址'), + }, + { + pattern: /^https?:\/\/.+/, + message: t('Gotify服务器地址必须以http://或https://开头'), + }, + ]} + /> + + handleFormChange('gotifyToken', val)} + prefix={} + extraText={t( + '在Gotify服务器创建应用后获得的令牌,用于发送通知', + )} + showClear + rules={[ + { + required: + notificationSettings.warningType === 'gotify', + message: t('请输入Gotify应用令牌'), + }, + ]} + /> + + + handleFormChange('gotifyPriority', val) + } + prefix={} + extraText={t('消息优先级,范围0-10,默认为5')} + style={{ width: '100%', maxWidth: '300px' }} + /> + +
+
+ {t('配置说明')} +
+
+
+ 1. {t('在Gotify服务器的应用管理中创建新应用')} +
+
+ 2.{' '} + {t( + '复制应用的令牌(Token)并填写到上方的应用令牌字段', + )} +
+
+ 3. {t('填写Gotify服务器的完整URL地址')} +
+
+ + {t('更多信息请参考')} + {' '} + + Gotify 官方文档 + +
+
+
+ + )} From d6db10b4bc5aeba46e400d0146fb527516d2304f Mon Sep 17 00:00:00 2001 From: RedwindA Date: Wed, 1 Oct 2025 19:36:19 +0800 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Bark=20?= =?UTF-8?q?=E5=92=8C=20Gotify=20=E9=80=9A=E7=9F=A5=E7=9A=84=E5=9B=BD?= =?UTF-8?q?=E9=99=85=E5=8C=96=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../personal/cards/NotificationSettings.jsx | 4 +-- web/src/i18n/locales/en.json | 32 +++++++++++++++++++ web/src/i18n/locales/fr.json | 32 +++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index dc428f145..0c99e2855 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -590,7 +590,7 @@ const NotificationSettings = ({ rel='noopener noreferrer' className='text-blue-500 hover:text-blue-600 font-medium' > - Bark 官方文档 + Bark {t('官方文档')} @@ -691,7 +691,7 @@ const NotificationSettings = ({ rel='noopener noreferrer' className='text-blue-500 hover:text-blue-600 font-medium' > - Gotify 官方文档 + Gotify {t('官方文档')} diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 1e1064b56..7ba76a0de 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1313,6 +1313,8 @@ "请输入Webhook地址,例如: https://example.com/webhook": "Please enter the Webhook URL, e.g.: https://example.com/webhook", "邮件通知": "Email notification", "Webhook通知": "Webhook notification", + "Bark通知": "Bark notification", + "Gotify通知": "Gotify notification", "接口凭证(可选)": "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", "Authorization: Bearer your-secret-key": "Authorization: Bearer your-secret-key", @@ -1323,6 +1325,36 @@ "通知邮箱": "Notification email", "设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "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", + "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", "对于官方渠道,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", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 3a216e53b..6dde55977 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -1308,6 +1308,8 @@ "请输入Webhook地址,例如: https://example.com/webhook": "Veuillez saisir l'URL du Webhook, par exemple : https://example.com/webhook", "邮件通知": "Notification par e-mail", "Webhook通知": "Notification par Webhook", + "Bark通知": "Notification Bark", + "Gotify通知": "Notification Gotify", "接口凭证(可选)": "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", "Authorization: Bearer your-secret-key": "Autorisation : Bearer votre-clé-secrète", @@ -1318,6 +1320,36 @@ "通知邮箱": "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", "留空则使用账号绑定的邮箱": "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", "对于官方渠道,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", From 0e9ad4a15f6cee8b3c0193215db1fa6bb7d5453d Mon Sep 17 00:00:00 2001 From: Seefs Date: Thu, 2 Oct 2025 00:14:35 +0800 Subject: [PATCH 09/11] fix: missing field & field control --- dto/channel_settings.go | 3 + dto/claude.go | 5 +- dto/openai_request.go | 39 ++++-- relay/claude_handler.go | 6 + relay/common/relay_info.go | 34 +++++ relay/compatible_handler.go | 6 + relay/responses_handler.go | 7 + .../channels/modals/EditChannelModal.jsx | 125 ++++++++++++++++-- web/src/i18n/locales/en.json | 7 + web/src/i18n/locales/fr.json | 7 + 10 files changed, 213 insertions(+), 26 deletions(-) diff --git a/dto/channel_settings.go b/dto/channel_settings.go index d6d6e0848..d57184b38 100644 --- a/dto/channel_settings.go +++ b/dto/channel_settings.go @@ -20,6 +20,9 @@ type ChannelOtherSettings struct { AzureResponsesVersion string `json:"azure_responses_version,omitempty"` VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key" 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 { diff --git a/dto/claude.go b/dto/claude.go index 427742263..dfc5cfd4c 100644 --- a/dto/claude.go +++ b/dto/claude.go @@ -195,12 +195,15 @@ type ClaudeRequest struct { Temperature *float64 `json:"temperature,omitempty"` TopP float64 `json:"top_p,omitempty"` TopK int `json:"top_k,omitempty"` - //ClaudeMetadata `json:"metadata,omitempty"` Stream bool `json:"stream,omitempty"` Tools any `json:"tools,omitempty"` ContextManagement json.RawMessage `json:"context_management,omitempty"` ToolChoice any `json:"tool_choice,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 { diff --git a/dto/openai_request.go b/dto/openai_request.go index 191fa638f..dbdfad446 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -57,6 +57,18 @@ type GeneralOpenAIRequest struct { Dimensions int `json:"dimensions,omitempty"` Modalities json.RawMessage `json:"modalities,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 ExtraBody json.RawMessage `json:"extra_body,omitempty"` //xai @@ -775,19 +787,20 @@ type OpenAIResponsesRequest struct { ParallelToolCalls json.RawMessage `json:"parallel_tool_calls,omitempty"` PreviousResponseID string `json:"previous_response_id,omitempty"` Reasoning *Reasoning `json:"reasoning,omitempty"` - ServiceTier string `json:"service_tier,omitempty"` - Store json.RawMessage `json:"store,omitempty"` - PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"` - Stream bool `json:"stream,omitempty"` - Temperature float64 `json:"temperature,omitempty"` - Text json.RawMessage `json:"text,omitempty"` - ToolChoice json.RawMessage `json:"tool_choice,omitempty"` - Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map - TopP float64 `json:"top_p,omitempty"` - Truncation string `json:"truncation,omitempty"` - User string `json:"user,omitempty"` - MaxToolCalls uint `json:"max_tool_calls,omitempty"` - Prompt json.RawMessage `json:"prompt,omitempty"` + // 服务层级字段,用于指定 API 服务等级。允许透传可能导致实际计费高于预期,默认应过滤 + ServiceTier string `json:"service_tier,omitempty"` + Store json.RawMessage `json:"store,omitempty"` + PromptCacheKey json.RawMessage `json:"prompt_cache_key,omitempty"` + Stream bool `json:"stream,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + Text json.RawMessage `json:"text,omitempty"` + ToolChoice json.RawMessage `json:"tool_choice,omitempty"` + Tools json.RawMessage `json:"tools,omitempty"` // 需要处理的参数很少,MCP 参数太多不确定,所以用 map + TopP float64 `json:"top_p,omitempty"` + Truncation string `json:"truncation,omitempty"` + User string `json:"user,omitempty"` + MaxToolCalls uint `json:"max_tool_calls,omitempty"` + Prompt json.RawMessage `json:"prompt,omitempty"` } func (r *OpenAIResponsesRequest) GetTokenCountMeta() *types.TokenCountMeta { diff --git a/relay/claude_handler.go b/relay/claude_handler.go index 59d12abe4..3a739785f 100644 --- a/relay/claude_handler.go +++ b/relay/claude_handler.go @@ -112,6 +112,12 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ 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 if len(info.ParamOverride) > 0 { jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride) diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index f4ffaee23..cb66cd806 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -507,3 +507,37 @@ type TaskInfo struct { Url string `json:"url,omitempty"` Progress string `json:"progress,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 { + return jsonData, err + } + + // 默认移除 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") + } + } + + return common.Marshal(data) +} diff --git a/relay/compatible_handler.go b/relay/compatible_handler.go index 38b820f72..a3ddf6d49 100644 --- a/relay/compatible_handler.go +++ b/relay/compatible_handler.go @@ -135,6 +135,12 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types 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 if len(info.ParamOverride) > 0 { jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride) diff --git a/relay/responses_handler.go b/relay/responses_handler.go index 0c57a303f..6958f96ef 100644 --- a/relay/responses_handler.go +++ b/relay/responses_handler.go @@ -56,6 +56,13 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError * if err != nil { 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 if len(info.ParamOverride) > 0 { jsonData, err = relaycommon.ApplyParamOverride(jsonData, info.ParamOverride) diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index f625ab14e..571c136f9 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -169,6 +169,10 @@ const EditChannelModal = (props) => { vertex_key_type: 'json', // 企业账户设置 is_enterprise_account: false, + // 字段透传控制默认值 + allow_service_tier: false, + disable_store: false, // false = 允许透传(默认开启) + allow_safety_identifier: false, }; const [batch, setBatch] = useState(false); const [multiToSingle, setMultiToSingle] = useState(false); @@ -453,17 +457,27 @@ const EditChannelModal = (props) => { data.vertex_key_type = parsedSettings.vertex_key_type || 'json'; // 读取企业账户设置 data.is_enterprise_account = parsedSettings.openrouter_enterprise === true; + // 读取字段透传控制设置 + data.allow_service_tier = parsedSettings.allow_service_tier || false; + data.disable_store = parsedSettings.disable_store || false; + data.allow_safety_identifier = parsedSettings.allow_safety_identifier || false; } catch (error) { console.error('解析其他设置失败:', error); data.azure_responses_version = ''; data.region = ''; data.vertex_key_type = 'json'; data.is_enterprise_account = false; + data.allow_service_tier = false; + data.disable_store = false; + data.allow_safety_identifier = false; } } else { // 兼容历史数据:老渠道没有 settings 时,默认按 json 展示 data.vertex_key_type = 'json'; data.is_enterprise_account = false; + data.allow_service_tier = false; + data.disable_store = false; + data.allow_safety_identifier = false; } if ( @@ -900,21 +914,33 @@ const EditChannelModal = (props) => { }; localInputs.setting = JSON.stringify(channelExtraSettings); - // 处理type === 20的企业账户设置 - if (localInputs.type === 20) { - let settings = {}; - if (localInputs.settings) { - try { - settings = JSON.parse(localInputs.settings); - } catch (error) { - console.error('解析settings失败:', error); - } + // 处理 settings 字段(包括企业账户设置和字段透传控制) + let settings = {}; + if (localInputs.settings) { + try { + settings = JSON.parse(localInputs.settings); + } catch (error) { + console.error('解析settings失败:', error); } - // 设置企业账户标识,无论是true还是false都要传到后端 - settings.openrouter_enterprise = localInputs.is_enterprise_account === true; - localInputs.settings = JSON.stringify(settings); } + // type === 20: 设置企业账户标识,无论是true还是false都要传到后端 + if (localInputs.type === 20) { + settings.openrouter_enterprise = localInputs.is_enterprise_account === true; + } + + // type === 1 (OpenAI) 或 type === 14 (Claude): 设置字段透传控制(显式保存布尔值) + if (localInputs.type === 1 || localInputs.type === 14) { + settings.allow_service_tier = localInputs.allow_service_tier === true; + // 仅 OpenAI 渠道需要 store 和 safety_identifier + if (localInputs.type === 1) { + settings.disable_store = localInputs.disable_store === true; + settings.allow_safety_identifier = localInputs.allow_safety_identifier === true; + } + } + + localInputs.settings = JSON.stringify(settings); + // 清理不需要发送到后端的字段 delete localInputs.force_format; delete localInputs.thinking_to_content; @@ -925,6 +951,10 @@ const EditChannelModal = (props) => { delete localInputs.is_enterprise_account; // 顶层的 vertex_key_type 不应发送给后端 delete localInputs.vertex_key_type; + // 清理字段透传控制的临时字段 + delete localInputs.allow_service_tier; + delete localInputs.disable_store; + delete localInputs.allow_safety_identifier; let res; localInputs.auto_ban = localInputs.auto_ban ? 1 : 0; @@ -2384,6 +2414,76 @@ const EditChannelModal = (props) => { '键为原状态码,值为要复写的状态码,仅影响本地判断', )} /> + + {/* 字段透传控制 - OpenAI 渠道 */} + {inputs.type === 1 && ( + <> +
+ {t('字段透传控制')} +
+ + + handleChannelOtherSettingsChange('allow_service_tier', value) + } + extraText={t( + 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', + )} + /> + + + handleChannelOtherSettingsChange('disable_store', value) + } + extraText={t( + 'store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用', + )} + /> + + + handleChannelOtherSettingsChange('allow_safety_identifier', value) + } + extraText={t( + 'safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私', + )} + /> + + )} + + {/* 字段透传控制 - Claude 渠道 */} + {(inputs.type === 14) && ( + <> +
+ {t('字段透传控制')} +
+ + + handleChannelOtherSettingsChange('allow_service_tier', value) + } + extraText={t( + 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', + )} + /> + + )} {/* Channel Extra Settings Card */} @@ -2487,6 +2587,7 @@ const EditChannelModal = (props) => { '如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面', )} /> + diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 1e1064b56..0d940d82b 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -2191,6 +2191,13 @@ "输入 Origin 后回车,如:https://example.com": "Enter Origin and press Enter, e.g.: https://example.com", "保存 Passkey 设置": "Save Passkey Settings", "黑名单": "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": { "changeLanguage": "Change Language" } diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 3a216e53b..f67b88efb 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -2135,6 +2135,13 @@ "关闭侧边栏": "Fermer la barre latérale", "定价": "Tarification", "语言": "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": { "changeLanguage": "Changer de langue" } From 26a563da54c65f4ade601b567a51da86013e0537 Mon Sep 17 00:00:00 2001 From: Seefs Date: Thu, 2 Oct 2025 13:57:49 +0800 Subject: [PATCH 10/11] fix: Return the original payload and nil error on Unmarshal or Marshal failures in RemoveDisabledFields --- relay/common/relay_info.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index cb66cd806..35f8ad191 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -515,7 +515,8 @@ type TaskInfo struct { func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOtherSettings) ([]byte, error) { var data map[string]interface{} if err := common.Unmarshal(jsonData, &data); err != nil { - return jsonData, err + common.SysError("RemoveDisabledFields Unmarshal error :" + err.Error()) + return jsonData, nil } // 默认移除 service_tier,除非明确允许(避免额外计费风险) @@ -539,5 +540,10 @@ func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOther } } - return common.Marshal(data) + jsonDataAfter, err := common.Marshal(data) + if err != nil { + common.SysError("RemoveDisabledFields Marshal error :" + err.Error()) + return jsonData, nil + } + return jsonDataAfter, nil } From 6a1de0ebdca9ffaa665ffb0346a04b30986a50e5 Mon Sep 17 00:00:00 2001 From: Seefs Date: Thu, 2 Oct 2025 14:28:58 +0800 Subject: [PATCH 11/11] fix: merge conflict --- .../components/table/channels/modals/EditChannelModal.jsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 5cff89616..09cfb0f05 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -2521,8 +2521,6 @@ const EditChannelModal = (props) => { '键为原状态码,值为要复写的状态码,仅影响本地判断', )} /> - - {/* 字段透传控制 - OpenAI 渠道 */} {inputs.type === 1 && ( @@ -2593,7 +2591,8 @@ const EditChannelModal = (props) => { /> )} - + + {/* Channel Extra Settings Card */}
formSectionRefs.current.channelExtraSettings = el}> @@ -2699,8 +2698,6 @@ const EditChannelModal = (props) => { />
- - )}