diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go index ccde91db..9a568d85 100644 --- a/controller/topup_stripe.go +++ b/controller/topup_stripe.go @@ -225,7 +225,8 @@ func genStripeLink(referenceId string, customerId string, email string, amount i Quantity: stripe.Int64(amount), }, }, - Mode: stripe.String(string(stripe.CheckoutSessionModePayment)), + Mode: stripe.String(string(stripe.CheckoutSessionModePayment)), + AllowPromotionCodes: stripe.Bool(setting.StripePromotionCodesEnabled), } if "" == customerId { diff --git a/dto/gemini.go b/dto/gemini.go index bc05c6aa..80552aad 100644 --- a/dto/gemini.go +++ b/dto/gemini.go @@ -251,6 +251,7 @@ type GeminiChatTool struct { GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"` CodeExecution any `json:"codeExecution,omitempty"` FunctionDeclarations any `json:"functionDeclarations,omitempty"` + URLContext any `json:"urlContext,omitempty"` } type GeminiChatGenerationConfig struct { diff --git a/main.go b/main.go index b1421f9e..e12dddc5 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "embed" "fmt" "log" @@ -16,6 +17,7 @@ import ( "one-api/setting/ratio_setting" "os" "strconv" + "strings" "time" "github.com/bytedance/gopkg/util/gopool" @@ -147,6 +149,22 @@ func main() { }) server.Use(sessions.Sessions("session", store)) + analyticsInjectBuilder := &strings.Builder{} + if os.Getenv("UMAMI_WEBSITE_ID") != "" { + umamiSiteID := os.Getenv("UMAMI_WEBSITE_ID") + umamiScriptURL := os.Getenv("UMAMI_SCRIPT_URL") + if umamiScriptURL == "" { + umamiScriptURL = "https://analytics.umami.is/script.js" + } + analyticsInjectBuilder.WriteString("") + } + analyticsInject := analyticsInjectBuilder.String() + indexPage = bytes.ReplaceAll(indexPage, []byte("\n"), []byte(analyticsInject)) + router.SetRouter(server, buildFS, indexPage) var port = os.Getenv("PORT") if port == "" { diff --git a/model/option.go b/model/option.go index ceecff65..9ace8fec 100644 --- a/model/option.go +++ b/model/option.go @@ -82,6 +82,7 @@ func InitOptionMap() { common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret common.OptionMap["StripePriceId"] = setting.StripePriceId common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64) + common.OptionMap["StripePromotionCodesEnabled"] = strconv.FormatBool(setting.StripePromotionCodesEnabled) common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString() common.OptionMap["Chats"] = setting.Chats2JsonString() common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString() @@ -330,6 +331,8 @@ func updateOptionMap(key string, value string) (err error) { setting.StripeUnitPrice, _ = strconv.ParseFloat(value, 64) case "StripeMinTopUp": setting.StripeMinTopUp, _ = strconv.Atoi(value) + case "StripePromotionCodesEnabled": + setting.StripePromotionCodesEnabled = value == "true" case "TopupGroupRatio": err = common.UpdateTopupGroupRatioByJSONString(value) case "GitHubClientId": diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index 199c8466..c8e9c757 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -245,6 +245,7 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i functions := make([]dto.FunctionRequest, 0, len(textRequest.Tools)) googleSearch := false codeExecution := false + urlContext := false for _, tool := range textRequest.Tools { if tool.Function.Name == "googleSearch" { googleSearch = true @@ -254,6 +255,10 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i codeExecution = true continue } + if tool.Function.Name == "urlContext" { + urlContext = true + continue + } if tool.Function.Parameters != nil { params, ok := tool.Function.Parameters.(map[string]interface{}) @@ -281,6 +286,11 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i GoogleSearch: make(map[string]string), }) } + if urlContext { + geminiTools = append(geminiTools, dto.GeminiChatTool{ + URLContext: make(map[string]string), + }) + } if len(functions) > 0 { geminiTools = append(geminiTools, dto.GeminiChatTool{ FunctionDeclarations: functions, diff --git a/setting/payment_stripe.go b/setting/payment_stripe.go index 80d877df..d97120c8 100644 --- a/setting/payment_stripe.go +++ b/setting/payment_stripe.go @@ -5,3 +5,4 @@ var StripeWebhookSecret = "" var StripePriceId = "" var StripeUnitPrice = 8.0 var StripeMinTopUp = 1 +var StripePromotionCodesEnabled = false diff --git a/web/index.html b/web/index.html index 09d87ae1..df6b0e39 100644 --- a/web/index.html +++ b/web/index.html @@ -10,6 +10,7 @@ content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" /> New API + diff --git a/web/src/components/settings/PaymentSetting.jsx b/web/src/components/settings/PaymentSetting.jsx index faaa9561..220c8664 100644 --- a/web/src/components/settings/PaymentSetting.jsx +++ b/web/src/components/settings/PaymentSetting.jsx @@ -45,6 +45,7 @@ const PaymentSetting = () => { StripePriceId: '', StripeUnitPrice: 8.0, StripeMinTopUp: 1, + StripePromotionCodesEnabled: false, }); let [loading, setLoading] = useState(false); diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 25ef68c6..2eb480e7 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -455,6 +455,14 @@ const EditChannelModal = (props) => { data.is_enterprise_account = false; } + if ( + data.type === 45 && + (!data.base_url || + (typeof data.base_url === 'string' && data.base_url.trim() === '')) + ) { + data.base_url = 'https://ark.cn-beijing.volces.com'; + } + setInputs(data); if (formApiRef.current) { formApiRef.current.setValues(data); @@ -837,7 +845,9 @@ const EditChannelModal = (props) => { delete localInputs.key; } } else { - localInputs.key = batch ? JSON.stringify(keys) : JSON.stringify(keys[0]); + localInputs.key = batch + ? JSON.stringify(keys) + : JSON.stringify(keys[0]); } } } @@ -954,6 +964,56 @@ const EditChannelModal = (props) => { } }; + // 密钥去重函数 + const deduplicateKeys = () => { + const currentKey = formApiRef.current?.getValue('key') || inputs.key || ''; + + if (!currentKey.trim()) { + showInfo(t('请先输入密钥')); + return; + } + + // 按行分割密钥 + const keyLines = currentKey.split('\n'); + const beforeCount = keyLines.length; + + // 使用哈希表去重,保持原有顺序 + const keySet = new Set(); + const deduplicatedKeys = []; + + keyLines.forEach((line) => { + const trimmedLine = line.trim(); + if (trimmedLine && !keySet.has(trimmedLine)) { + keySet.add(trimmedLine); + deduplicatedKeys.push(trimmedLine); + } + }); + + const afterCount = deduplicatedKeys.length; + const deduplicatedKeyText = deduplicatedKeys.join('\n'); + + // 更新表单和状态 + if (formApiRef.current) { + formApiRef.current.setValue('key', deduplicatedKeyText); + } + handleInputChange('key', deduplicatedKeyText); + + // 显示去重结果 + const message = t( + '去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥', + { + before: beforeCount, + after: afterCount, + }, + ); + + if (beforeCount === afterCount) { + showInfo(t('未发现重复密钥')); + } else { + showSuccess(message); + } + }; + const addCustomModels = () => { if (customModel.trim() === '') return; const modelArray = customModel.split(',').map((model) => model.trim()); @@ -1049,24 +1109,41 @@ const EditChannelModal = (props) => { )} {batch && ( - { - setMultiToSingle((prev) => !prev); - setInputs((prev) => { - const newInputs = { ...prev }; - if (!multiToSingle) { - newInputs.multi_key_mode = multiKeyMode; - } else { - delete newInputs.multi_key_mode; - } - return newInputs; - }); - }} - > - {t('密钥聚合模式')} - + <> + { + setMultiToSingle((prev) => { + const nextValue = !prev; + setInputs((prevInputs) => { + const newInputs = { ...prevInputs }; + if (nextValue) { + newInputs.multi_key_mode = multiKeyMode; + } else { + delete newInputs.multi_key_mode; + } + return newInputs; + }); + return nextValue; + }); + }} + > + {t('密钥聚合模式')} + + + {inputs.type !== 41 && ( + + )} + )} ) : null; @@ -1268,7 +1345,10 @@ const EditChannelModal = (props) => { value={inputs.vertex_key_type || 'json'} onChange={(value) => { // 更新设置中的 vertex_key_type - handleChannelOtherSettingsChange('vertex_key_type', value); + handleChannelOtherSettingsChange( + 'vertex_key_type', + value, + ); // 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件 if (value === 'api_key') { setBatch(false); @@ -1288,7 +1368,8 @@ const EditChannelModal = (props) => { /> )} {batch ? ( - inputs.type === 41 && (inputs.vertex_key_type || 'json') === 'json' ? ( + inputs.type === 41 && + (inputs.vertex_key_type || 'json') === 'json' ? ( { autoComplete='new-password' onChange={(value) => handleInputChange('key', value)} extraText={ -
+
{isEdit && isMultiKeyChannel && keyMode === 'append' && ( @@ -1352,7 +1433,8 @@ const EditChannelModal = (props) => { ) ) : ( <> - {inputs.type === 41 && (inputs.vertex_key_type || 'json') === 'json' ? ( + {inputs.type === 41 && + (inputs.vertex_key_type || 'json') === 'json' ? ( <> {!batch && (
diff --git a/web/src/components/table/task-logs/modals/ContentModal.jsx b/web/src/components/table/task-logs/modals/ContentModal.jsx index 3d747b77..3bfba37b 100644 --- a/web/src/components/table/task-logs/modals/ContentModal.jsx +++ b/web/src/components/table/task-logs/modals/ContentModal.jsx @@ -17,8 +17,11 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React from 'react'; -import { Modal } from '@douyinfe/semi-ui'; +import React, { useState, useEffect } from 'react'; +import { Modal, Button, Typography, Spin } from '@douyinfe/semi-ui'; +import { IconExternalOpen, IconCopy } from '@douyinfe/semi-icons'; + +const { Text } = Typography; const ContentModal = ({ isModalOpen, @@ -26,17 +29,120 @@ const ContentModal = ({ modalContent, isVideo, }) => { + const [videoError, setVideoError] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (isModalOpen && isVideo) { + setVideoError(false); + setIsLoading(true); + } + }, [isModalOpen, isVideo]); + + const handleVideoError = () => { + setVideoError(true); + setIsLoading(false); + }; + + const handleVideoLoaded = () => { + setIsLoading(false); + }; + + const handleCopyUrl = () => { + navigator.clipboard.writeText(modalContent); + }; + + const handleOpenInNewTab = () => { + window.open(modalContent, '_blank'); + }; + + const renderVideoContent = () => { + if (videoError) { + return ( +
+ + 视频无法在当前浏览器中播放,这可能是由于: + + + • 视频服务商的跨域限制 + + + • 需要特定的请求头或认证 + + + • 防盗链保护机制 + + +
+ + +
+ +
+ + {modalContent} + +
+
+ ); + } + + return ( +
+ {isLoading && ( +
+ +
+ )} +
+ ); + }; + return ( setIsModalOpen(false)} onCancel={() => setIsModalOpen(false)} closable={null} - bodyStyle={{ height: '400px', overflow: 'auto' }} + bodyStyle={{ + height: isVideo ? '450px' : '400px', + overflow: 'auto', + padding: isVideo && videoError ? '0' : '24px' + }} width={800} > {isVideo ? ( -