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 (
+
+
+ 视频无法在当前浏览器中播放,这可能是由于:
+
+
+ • 视频服务商的跨域限制
+
+
+ • 需要特定的请求头或认证
+
+
+ • 防盗链保护机制
+
+
+
+ }
+ onClick={handleOpenInNewTab}
+ style={{ marginRight: '8px' }}
+ >
+ 在新标签页中打开
+
+ }
+ onClick={handleCopyUrl}
+ >
+ 复制链接
+
+
+
+
+
+ {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 ? (
-
+ renderVideoContent()
) : (
{modalContent}
)}
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index ceb0f2d3..e935c10c 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -837,6 +837,7 @@
"确定要充值 $": "Confirm to top up $",
"微信/支付宝 实付金额:": "WeChat/Alipay actual payment amount:",
"Stripe 实付金额:": "Stripe actual payment amount:",
+ "允许在 Stripe 支付中输入促销码": "Allow entering promotion codes during Stripe checkout",
"支付中...": "Paying",
"支付宝": "Alipay",
"收益统计": "Income statistics",
diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json
index 95fa0641..4b6b1e68 100644
--- a/web/src/i18n/locales/zh.json
+++ b/web/src/i18n/locales/zh.json
@@ -32,5 +32,6 @@
"端口配置详细说明": "限制外部请求只能访问指定端口。支持单个端口(80, 443)或端口范围(8000-8999)。空列表允许所有端口。默认包含常用Web端口。",
"输入端口后回车,如:80 或 8000-8999": "输入端口后回车,如:80 或 8000-8999",
"更新SSRF防护设置": "更新SSRF防护设置",
- "域名IP过滤详细说明": "⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。"
+ "域名IP过滤详细说明": "⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。",
+ "允许在 Stripe 支付中输入促销码": "允许在 Stripe 支付中输入促销码"
}
diff --git a/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx b/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx
index 2f4ea210..e4ddea11 100644
--- a/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx
+++ b/web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx
@@ -45,6 +45,7 @@ export default function SettingsPaymentGateway(props) {
StripePriceId: '',
StripeUnitPrice: 8.0,
StripeMinTopUp: 1,
+ StripePromotionCodesEnabled: false,
});
const [originInputs, setOriginInputs] = useState({});
const formApiRef = useRef(null);
@@ -63,6 +64,10 @@ export default function SettingsPaymentGateway(props) {
props.options.StripeMinTopUp !== undefined
? parseFloat(props.options.StripeMinTopUp)
: 1,
+ StripePromotionCodesEnabled:
+ props.options.StripePromotionCodesEnabled !== undefined
+ ? props.options.StripePromotionCodesEnabled
+ : false,
};
setInputs(currentInputs);
setOriginInputs({ ...currentInputs });
@@ -114,6 +119,16 @@ export default function SettingsPaymentGateway(props) {
value: inputs.StripeMinTopUp.toString(),
});
}
+ if (
+ originInputs['StripePromotionCodesEnabled'] !==
+ inputs.StripePromotionCodesEnabled &&
+ inputs.StripePromotionCodesEnabled !== undefined
+ ) {
+ options.push({
+ key: 'StripePromotionCodesEnabled',
+ value: inputs.StripePromotionCodesEnabled ? 'true' : 'false',
+ });
+ }
// 发送请求
const requestQueue = options.map((opt) =>
@@ -225,6 +240,15 @@ export default function SettingsPaymentGateway(props) {
placeholder={t('例如:2,就是最低充值2$')}
/>
+
+
+