Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a706f00287 | |||
| 7efb1922fe | |||
| 89fe99f3bd | |||
| e5b5331d3b | |||
| 18373c6eac | |||
| 5b47011e08 | |||
| ab99c30884 | |||
| 670abee2f0 | |||
| 8bb9a42f68 | |||
| d22f889e5d | |||
| 3734059da7 | |||
| 26ce873f8b | |||
| e099117c61 | |||
| 310d618a16 | |||
| 20399d3c8f | |||
| 53aeee4ff7 | |||
| b2dd4acc9f | |||
| 4e492b26f6 | |||
| 82b750398c | |||
| 814a3f5124 |
+11
-8
@@ -70,17 +70,20 @@
|
||||
<p align="center">
|
||||
<a href="https://www.cherry-ai.com/" target="_blank">
|
||||
<img src="./docs/images/cherry-studio.png" alt="Cherry Studio" height="80" />
|
||||
</a>
|
||||
<a href="https://bda.pku.edu.cn/" target="_blank">
|
||||
</a><!--
|
||||
--><a href="https://github.com/iOfficeAI/AionUi/" target="_blank">
|
||||
<img src="./docs/images/aionui.png" alt="Aion UI" height="80" />
|
||||
</a><!--
|
||||
--><a href="https://bda.pku.edu.cn/" target="_blank">
|
||||
<img src="./docs/images/pku.png" alt="北京大學" height="80" />
|
||||
</a>
|
||||
<a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target="_blank">
|
||||
</a><!--
|
||||
--><a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target="_blank">
|
||||
<img src="./docs/images/ucloud.png" alt="UCloud 優刻得" height="80" />
|
||||
</a>
|
||||
<a href="https://www.aliyun.com/" target="_blank">
|
||||
</a><!--
|
||||
--><a href="https://www.aliyun.com/" target="_blank">
|
||||
<img src="./docs/images/aliyun.png" alt="阿里雲" height="80" />
|
||||
</a>
|
||||
<a href="https://io.net/" target="_blank">
|
||||
</a><!--
|
||||
--><a href="https://io.net/" target="_blank">
|
||||
<img src="./docs/images/io-net.png" alt="IO.NET" height="80" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
+14
-21
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/oauth"
|
||||
@@ -116,7 +117,6 @@ func GetStatus(c *gin.Context) {
|
||||
"user_agreement_enabled": legalSetting.UserAgreement != "",
|
||||
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
|
||||
"checkin_enabled": operation_setting.GetCheckinSetting().Enabled,
|
||||
"_qn": "new-api",
|
||||
}
|
||||
|
||||
// 根据启用状态注入可选内容
|
||||
@@ -308,31 +308,24 @@ func SendPasswordResetEmail(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if !model.IsEmailAlreadyTaken(email) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "该邮箱地址未注册",
|
||||
})
|
||||
return
|
||||
}
|
||||
code := common.GenerateVerificationCode(0)
|
||||
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
|
||||
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", system_setting.ServerAddress, email, code)
|
||||
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
||||
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
||||
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
|
||||
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
|
||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
|
||||
err := common.SendEmail(subject, email, content)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
if model.IsEmailAlreadyTaken(email) {
|
||||
code := common.GenerateVerificationCode(0)
|
||||
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
|
||||
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", system_setting.ServerAddress, email, code)
|
||||
subject := fmt.Sprintf("%s密码重置", common.SystemName)
|
||||
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
|
||||
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
|
||||
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
|
||||
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
|
||||
err := common.SendEmail(subject, email, content)
|
||||
if err != nil {
|
||||
logger.LogError(c.Request.Context(), fmt.Sprintf("failed to send password reset email to %s: %s", email, err.Error()))
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
type PasswordResetRequest struct {
|
||||
|
||||
@@ -46,7 +46,7 @@ func GetPricing(c *gin.Context) {
|
||||
"usable_group": usableGroup,
|
||||
"supported_endpoint": model.GetSupportedEndpointMap(),
|
||||
"auto_groups": service.GetUserAutoGroup(group),
|
||||
"_": "a42d372ccf0b5dd13ecf71203521f9d2",
|
||||
"pricing_version": "a42d372ccf0b5dd13ecf71203521f9d2",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+12
-2
@@ -925,9 +925,19 @@ func ManageUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
type emailBindRequest struct {
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func EmailBind(c *gin.Context) {
|
||||
email := c.Query("email")
|
||||
code := c.Query("code")
|
||||
var req emailBindRequest
|
||||
if err := common.DecodeJson(c.Request.Body, &req); err != nil {
|
||||
common.ApiError(c, errors.New("invalid request body"))
|
||||
return
|
||||
}
|
||||
email := req.Email
|
||||
code := req.Code
|
||||
if !common.VerifyCodeWithKey(email, code, common.EmailVerificationPurpose) {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserVerificationCodeError)
|
||||
return
|
||||
|
||||
@@ -10,10 +10,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -127,6 +129,13 @@ func VideoProxy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
fetchSetting := system_setting.GetFetchSetting()
|
||||
if err := common.ValidateURLWithFetchSetting(videoURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
|
||||
logger.LogError(c.Request.Context(), fmt.Sprintf("Video URL blocked for task %s: %v", taskID, err))
|
||||
videoProxyError(c, http.StatusForbidden, "server_error", fmt.Sprintf("request blocked: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
req.URL, err = url.Parse(videoURL)
|
||||
if err != nil {
|
||||
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to parse URL %s: %s", videoURL, err.Error()))
|
||||
|
||||
+15
-2
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -25,7 +26,7 @@ func getWeChatIdByCode(code string) (string, error) {
|
||||
if code == "" {
|
||||
return "", errors.New("无效的参数")
|
||||
}
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/user?code=%s", common.WeChatServerAddress, code), nil)
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/user?code=%s", common.WeChatServerAddress, url.QueryEscape(code)), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -121,6 +122,10 @@ func WeChatAuth(c *gin.Context) {
|
||||
setupLogin(&user, c)
|
||||
}
|
||||
|
||||
type wechatBindRequest struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
func WeChatBind(c *gin.Context) {
|
||||
if !common.WeChatAuthEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -129,7 +134,15 @@ func WeChatBind(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
code := c.Query("code")
|
||||
var req wechatBindRequest
|
||||
if err := common.DecodeJson(c.Request.Body, &req); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的请求",
|
||||
})
|
||||
return
|
||||
}
|
||||
code := req.Code
|
||||
wechatId, err := getWeChatIdByCode(code)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
|
||||
+5
-6
@@ -148,15 +148,14 @@ func (i *ImageRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
}
|
||||
}
|
||||
|
||||
// not support token count for dalle
|
||||
n := uint(1)
|
||||
if i.N != nil {
|
||||
n = *i.N
|
||||
}
|
||||
// n is NOT included here; it is handled via OtherRatio("n") in
|
||||
// image_handler.go (default) or channel adaptors (actual count).
|
||||
// Including n here caused double-counting for channels that also
|
||||
// set OtherRatio("n") (e.g. Ali/Bailian).
|
||||
return &types.TokenCountMeta{
|
||||
CombineText: i.Prompt,
|
||||
MaxTokens: 1584,
|
||||
ImagePriceRatio: sizeRatio * qualityRatio * float64(n),
|
||||
ImagePriceRatio: sizeRatio * qualityRatio,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -3948,9 +3948,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -171,12 +171,17 @@ type AliImageRequest struct {
|
||||
}
|
||||
|
||||
type AliImageParameters struct {
|
||||
Size string `json:"size,omitempty"`
|
||||
N int `json:"n,omitempty"`
|
||||
Steps string `json:"steps,omitempty"`
|
||||
Scale string `json:"scale,omitempty"`
|
||||
Watermark *bool `json:"watermark,omitempty"`
|
||||
PromptExtend *bool `json:"prompt_extend,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
N int `json:"n,omitempty"`
|
||||
Steps string `json:"steps,omitempty"`
|
||||
Scale string `json:"scale,omitempty"`
|
||||
Watermark *bool `json:"watermark,omitempty"`
|
||||
PromptExtend *bool `json:"prompt_extend,omitempty"`
|
||||
ThinkingMode *bool `json:"thinking_mode,omitempty"`
|
||||
EnableSequential *bool `json:"enable_sequential,omitempty"`
|
||||
BboxList any `json:"bbox_list,omitempty"`
|
||||
ColorPalette any `json:"color_palette,omitempty"`
|
||||
Seed *int `json:"seed,omitempty"`
|
||||
}
|
||||
|
||||
func (p *AliImageParameters) PromptExtendValue() bool {
|
||||
|
||||
@@ -54,7 +54,6 @@ func oaiImage2AliImageRequest(info *relaycommon.RelayInfo, request dto.ImageRequ
|
||||
}
|
||||
}
|
||||
|
||||
// 检查n参数
|
||||
if imageRequest.Parameters.N != 0 {
|
||||
info.PriceData.AddOtherRatio("n", float64(imageRequest.Parameters.N))
|
||||
}
|
||||
@@ -181,6 +180,7 @@ func oaiFormEdit2AliImageEdit(c *gin.Context, info *relaycommon.RelayInfo, reque
|
||||
},
|
||||
}
|
||||
imageRequest.Parameters = AliImageParameters{
|
||||
N: int(lo.FromPtrOr(request.N, uint(1))),
|
||||
Watermark: request.Watermark,
|
||||
}
|
||||
return &imageRequest, nil
|
||||
@@ -328,7 +328,6 @@ func aliImageHandler(a *Adaptor, c *gin.Context, resp *http.Response, info *rela
|
||||
}
|
||||
|
||||
imageResponses := responseAli2OpenAIImage(c, aliResponse, originRespBody, info, responseFormat)
|
||||
// 可能生成多张图片,修正计费数量n
|
||||
if aliResponse.Usage.ImageCount != 0 {
|
||||
info.PriceData.AddOtherRatio("n", float64(aliResponse.Usage.ImageCount))
|
||||
} else if len(imageResponses.Data) != 0 {
|
||||
|
||||
@@ -40,7 +40,8 @@ func oaiFormEdit2WanxImageEdit(c *gin.Context, info *relaycommon.RelayInfo, requ
|
||||
}
|
||||
|
||||
func isOldWanModel(modelName string) bool {
|
||||
return strings.Contains(modelName, "wan") && !strings.Contains(modelName, "wan2.6")
|
||||
return strings.Contains(modelName, "wan") &&
|
||||
!lo.SomeBy([]string{"wan2.6", "wan2.7"}, func(v string) bool { return strings.Contains(modelName, v) })
|
||||
}
|
||||
|
||||
func isWanModel(modelName string) bool {
|
||||
|
||||
@@ -76,7 +76,7 @@ func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayIn
|
||||
if strings.HasPrefix(request.Model, "grok-3-mini") {
|
||||
if lo.FromPtrOr(request.MaxCompletionTokens, uint(0)) == 0 && lo.FromPtrOr(request.MaxTokens, uint(0)) != 0 {
|
||||
request.MaxCompletionTokens = request.MaxTokens
|
||||
request.MaxTokens = lo.ToPtr(uint(0))
|
||||
request.MaxTokens = nil
|
||||
}
|
||||
if strings.HasSuffix(request.Model, "-high") {
|
||||
request.ReasoningEffort = "high"
|
||||
|
||||
+11
-2
@@ -117,11 +117,20 @@ func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type
|
||||
if request.N != nil {
|
||||
imageN = *request.N
|
||||
}
|
||||
|
||||
// n is handled via OtherRatio so it is applied exactly once in quota
|
||||
// calculation (both price-based and ratio-based paths).
|
||||
// Adaptors may have already set a more accurate count from the
|
||||
// upstream response; only set the default when they haven't.
|
||||
if _, hasN := info.PriceData.OtherRatios["n"]; !hasN {
|
||||
info.PriceData.AddOtherRatio("n", float64(imageN))
|
||||
}
|
||||
|
||||
if usage.(*dto.Usage).TotalTokens == 0 {
|
||||
usage.(*dto.Usage).TotalTokens = int(imageN)
|
||||
usage.(*dto.Usage).TotalTokens = 1
|
||||
}
|
||||
if usage.(*dto.Usage).PromptTokens == 0 {
|
||||
usage.(*dto.Usage).PromptTokens = int(imageN)
|
||||
usage.(*dto.Usage).PromptTokens = 1
|
||||
}
|
||||
|
||||
quality := "standard"
|
||||
|
||||
@@ -49,6 +49,13 @@ func RelayMidjourneyImage(c *gin.Context) {
|
||||
if httpClient == nil {
|
||||
httpClient = service.GetHttpClient()
|
||||
}
|
||||
fetchSetting := system_setting.GetFetchSetting()
|
||||
if err := common.ValidateURLWithFetchSetting(midjourneyTask.ImageUrl, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain); err != nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": fmt.Sprintf("request blocked: %v", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
resp, err := httpClient.Get(midjourneyTask.ImageUrl)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
|
||||
@@ -36,10 +36,10 @@ func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||
// OAuth routes - specific routes must come before :provider wildcard
|
||||
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
|
||||
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), controller.EmailBind)
|
||||
apiRouter.POST("/oauth/email/bind", middleware.CriticalRateLimit(), controller.EmailBind)
|
||||
// Non-standard OAuth (WeChat, Telegram) - keep original routes
|
||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), controller.WeChatBind)
|
||||
apiRouter.POST("/oauth/wechat/bind", middleware.CriticalRateLimit(), controller.WeChatBind)
|
||||
apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin)
|
||||
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind)
|
||||
// Standard OAuth providers (GitHub, Discord, OIDC, LinuxDO) - unified route
|
||||
@@ -226,7 +226,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
channelRoute.POST("/batch", controller.DeleteChannelBatch)
|
||||
channelRoute.POST("/fix", controller.FixChannelsAbilities)
|
||||
channelRoute.GET("/fetch_models/:id", controller.FetchUpstreamModels)
|
||||
channelRoute.POST("/fetch_models", controller.FetchModels)
|
||||
channelRoute.POST("/fetch_models", middleware.RootAuth(), controller.FetchModels)
|
||||
channelRoute.POST("/codex/oauth/start", controller.StartCodexOAuth)
|
||||
channelRoute.POST("/codex/oauth/complete", controller.CompleteCodexOAuth)
|
||||
channelRoute.POST("/:id/codex/oauth/start", controller.StartCodexOAuthForChannel)
|
||||
|
||||
@@ -17,6 +17,7 @@ var defaultQwenSettings = QwenSettings{
|
||||
"z-image",
|
||||
"qwen-image",
|
||||
"wan2.6",
|
||||
"wan2.7",
|
||||
"qwen-image-edit",
|
||||
"qwen-image-edit-max",
|
||||
"qwen-image-edit-max-2026-01-16",
|
||||
|
||||
@@ -21,7 +21,7 @@ var defaultFetchSetting = FetchSetting{
|
||||
DomainList: []string{},
|
||||
IpList: []string{},
|
||||
AllowedPorts: []string{"80", "443", "8080", "8443"},
|
||||
ApplyIPFilterForDomain: false,
|
||||
ApplyIPFilterForDomain: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -221,23 +221,27 @@ const FooterBar = () => {
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{footer ? (
|
||||
<div className='relative'>
|
||||
<div
|
||||
className='custom-footer'
|
||||
dangerouslySetInnerHTML={{ __html: footer }}
|
||||
></div>
|
||||
<div className='absolute bottom-2 right-4 text-xs !text-semi-color-text-2 opacity-70'>
|
||||
<span>{t('设计与开发由')} </span>
|
||||
<a
|
||||
href='https://github.com/QuantumNous/new-api'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='!text-semi-color-primary font-medium'
|
||||
>
|
||||
New API
|
||||
</a>
|
||||
<footer className='relative h-auto py-4 px-6 md:px-24 w-full flex items-center justify-center overflow-hidden'>
|
||||
<div className='flex flex-col md:flex-row items-center justify-between w-full max-w-[1110px] gap-4'>
|
||||
<div
|
||||
className='custom-footer na-cb6feafeb3990c78 text-sm !text-semi-color-text-1'
|
||||
dangerouslySetInnerHTML={{ __html: footer }}
|
||||
></div>
|
||||
<div className='text-sm flex-shrink-0'>
|
||||
<span className='!text-semi-color-text-1'>
|
||||
{t('设计与开发由')}{' '}
|
||||
</span>
|
||||
<a
|
||||
href='https://github.com/QuantumNous/new-api'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='!text-semi-color-primary font-medium'
|
||||
>
|
||||
New API
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
) : (
|
||||
customFooter
|
||||
)}
|
||||
|
||||
@@ -306,9 +306,9 @@ const PersonalSetting = () => {
|
||||
|
||||
const bindWeChat = async () => {
|
||||
if (inputs.wechat_verification_code === '') return;
|
||||
const res = await API.get(
|
||||
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
|
||||
);
|
||||
const res = await API.post('/api/oauth/wechat/bind', {
|
||||
code: inputs.wechat_verification_code,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess(t('微信账户绑定成功!'));
|
||||
@@ -378,9 +378,10 @@ const PersonalSetting = () => {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
|
||||
);
|
||||
const res = await API.post('/api/oauth/email/bind', {
|
||||
email: inputs.email,
|
||||
code: inputs.email_verification_code,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess(t('邮箱账户绑定成功!'));
|
||||
|
||||
@@ -108,7 +108,7 @@ const SystemSetting = () => {
|
||||
'fetch_setting.domain_list': [],
|
||||
'fetch_setting.ip_list': [],
|
||||
'fetch_setting.allowed_ports': [],
|
||||
'fetch_setting.apply_ip_filter_for_domain': false,
|
||||
'fetch_setting.apply_ip_filter_for_domain': true,
|
||||
});
|
||||
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
@@ -847,7 +847,7 @@ const SystemSetting = () => {
|
||||
}
|
||||
style={{ marginBottom: 8 }}
|
||||
>
|
||||
{t('对域名启用 IP 过滤(实验性)')}
|
||||
{t('对域名启用 IP 过滤(推荐开启)')}
|
||||
</Form.Checkbox>
|
||||
<Text strong>
|
||||
{t(domainFilterMode ? '域名白名单' : '域名黑名单')}
|
||||
|
||||
@@ -67,6 +67,7 @@ import SecureVerificationModal from '../../../common/modals/SecureVerificationMo
|
||||
import StatusCodeRiskGuardModal from './StatusCodeRiskGuardModal';
|
||||
import ChannelKeyDisplay from '../../../common/ui/ChannelKeyDisplay';
|
||||
import { useSecureVerification } from '../../../../hooks/common/useSecureVerification';
|
||||
import { parseChannelConnectionString } from '../../../../helpers/token';
|
||||
import { createApiCalls } from '../../../../services/secureVerification';
|
||||
import {
|
||||
collectInvalidStatusCodeEntries,
|
||||
@@ -102,6 +103,7 @@ const REGION_EXAMPLE = {
|
||||
'claude-3-5-sonnet-20240620': 'europe-west1',
|
||||
};
|
||||
const UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT = 8;
|
||||
const ADVANCED_SETTINGS_EXPANDED_KEY = 'channel-advanced-settings-expanded';
|
||||
|
||||
const PARAM_OVERRIDE_LEGACY_TEMPLATE = {
|
||||
temperature: 0,
|
||||
@@ -398,8 +400,15 @@ const EditChannelModal = (props) => {
|
||||
[],
|
||||
);
|
||||
|
||||
// 剪贴板连接信息自动检测
|
||||
const [clipboardConfig, setClipboardConfig] = useState(null);
|
||||
|
||||
// 高级设置折叠状态
|
||||
const [advancedSettingsOpen, setAdvancedSettingsOpen] = useState(false);
|
||||
const toggleAdvancedSettings = (open) => {
|
||||
setAdvancedSettingsOpen(open);
|
||||
localStorage.setItem(ADVANCED_SETTINGS_EXPANDED_KEY, String(open));
|
||||
};
|
||||
const formContainerRef = useRef(null);
|
||||
const doubaoApiClickCountRef = useRef(0);
|
||||
const initialBaseUrlRef = useRef('');
|
||||
@@ -538,6 +547,39 @@ const EditChannelModal = (props) => {
|
||||
handleInputChange('settings', settingsJson);
|
||||
};
|
||||
|
||||
const applyClipboardConfig = (config) => {
|
||||
if (!config) return;
|
||||
setInputs((prev) => ({
|
||||
...prev,
|
||||
key: config.key,
|
||||
base_url: config.url,
|
||||
}));
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('key', config.key);
|
||||
formApiRef.current.setValue('base_url', config.url);
|
||||
}
|
||||
setClipboardConfig(null);
|
||||
showSuccess(t('连接信息已填入'));
|
||||
};
|
||||
|
||||
const pasteFromClipboard = async () => {
|
||||
if (!navigator?.clipboard?.readText) {
|
||||
showError(t('无法读取剪贴板'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const parsed = parseChannelConnectionString(text);
|
||||
if (parsed) {
|
||||
applyClipboardConfig(parsed);
|
||||
} else {
|
||||
showInfo(t('剪贴板中未检测到连接信息'));
|
||||
}
|
||||
} catch {
|
||||
showError(t('无法读取剪贴板'));
|
||||
}
|
||||
};
|
||||
|
||||
const isIonetLocked = isIonetChannel && isEdit;
|
||||
|
||||
const handleInputChange = (name, value) => {
|
||||
@@ -1269,12 +1311,22 @@ const EditChannelModal = (props) => {
|
||||
loadChannel();
|
||||
} else {
|
||||
formApiRef.current?.setValues(getInitValues());
|
||||
try {
|
||||
navigator?.clipboard?.readText()?.then((text) => {
|
||||
const parsed = parseChannelConnectionString(text);
|
||||
if (parsed) {
|
||||
setClipboardConfig(parsed);
|
||||
}
|
||||
}).catch(() => {});
|
||||
} catch {}
|
||||
}
|
||||
fetchModelGroups();
|
||||
// 重置手动输入模式状态
|
||||
setUseManualInput(false);
|
||||
// 重置高级设置折叠状态
|
||||
setAdvancedSettingsOpen(false);
|
||||
// 编辑模式下恢复用户偏好,创建模式一律折叠
|
||||
setAdvancedSettingsOpen(
|
||||
isEdit && localStorage.getItem(ADVANCED_SETTINGS_EXPANDED_KEY) === 'true'
|
||||
);
|
||||
} else {
|
||||
// 统一的模态框关闭重置逻辑
|
||||
resetModalState();
|
||||
@@ -1329,6 +1381,8 @@ const EditChannelModal = (props) => {
|
||||
setInputs(getInitValues());
|
||||
// 重置密钥显示状态
|
||||
resetKeyDisplayState();
|
||||
// 重置剪贴板检测状态
|
||||
setClipboardConfig(null);
|
||||
};
|
||||
|
||||
const handleVertexUploadChange = ({ fileList }) => {
|
||||
@@ -2077,14 +2131,27 @@ const EditChannelModal = (props) => {
|
||||
<SideSheet
|
||||
placement={isEdit ? 'right' : 'left'}
|
||||
title={
|
||||
<Space>
|
||||
<Tag color='blue' shape='circle'>
|
||||
{isEdit ? t('编辑') : t('新建')}
|
||||
</Tag>
|
||||
<Title heading={4} className='m-0'>
|
||||
{isEdit ? t('更新渠道信息') : t('创建新的渠道')}
|
||||
</Title>
|
||||
</Space>
|
||||
<div className='flex items-center justify-between w-full'>
|
||||
<Space>
|
||||
<Tag color='blue' shape='circle'>
|
||||
{isEdit ? t('编辑') : t('新建')}
|
||||
</Tag>
|
||||
<Title heading={4} className='m-0'>
|
||||
{isEdit ? t('更新渠道信息') : t('创建新的渠道')}
|
||||
</Title>
|
||||
</Space>
|
||||
{!isEdit && (
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
className='ec-dbcd0a3c01b55203 shrink-0'
|
||||
icon={<IconBolt />}
|
||||
onClick={pasteFromClipboard}
|
||||
>
|
||||
{t('从剪贴板粘贴配置')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{ padding: '0' }}
|
||||
visible={props.visible}
|
||||
@@ -2446,6 +2513,34 @@ const EditChannelModal = (props) => {
|
||||
<>
|
||||
<Spin spinning={loading}>
|
||||
<div className='p-2 space-y-3' ref={formContainerRef}>
|
||||
{!isEdit && clipboardConfig && (
|
||||
<Banner
|
||||
type='info'
|
||||
className='ec-dbcd0a3c01b55203'
|
||||
description={
|
||||
<div className='flex items-center justify-between gap-2'>
|
||||
<span>{t('检测到剪贴板中的连接信息')}</span>
|
||||
<div className='flex gap-1'>
|
||||
<Button
|
||||
size='small'
|
||||
theme='solid'
|
||||
type='primary'
|
||||
onClick={() => applyClipboardConfig(clipboardConfig)}
|
||||
>
|
||||
{t('自动填入')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={() => setClipboardConfig(null)}
|
||||
>
|
||||
{t('忽略')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* Core Configuration Card - Always Visible */}
|
||||
<Card className='!rounded-2xl shadow-sm border-0'>
|
||||
{/* Header */}
|
||||
@@ -3548,7 +3643,7 @@ const EditChannelModal = (props) => {
|
||||
{isMobile ? (
|
||||
<Collapse
|
||||
activeKey={advancedSettingsOpen ? ['advanced'] : []}
|
||||
onChange={(keys) => setAdvancedSettingsOpen(keys.includes('advanced'))}
|
||||
onChange={(keys) => toggleAdvancedSettings(keys.includes('advanced'))}
|
||||
>
|
||||
<Collapse.Panel
|
||||
header={
|
||||
@@ -3570,7 +3665,7 @@ const EditChannelModal = (props) => {
|
||||
backgroundColor: advancedSettingsOpen ? 'var(--semi-color-primary-light-default)' : 'var(--semi-color-fill-0)',
|
||||
border: '1px solid var(--semi-color-fill-2)',
|
||||
}}
|
||||
onClick={() => setAdvancedSettingsOpen(!advancedSettingsOpen)}
|
||||
onClick={() => toggleAdvancedSettings(!advancedSettingsOpen)}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<IconSetting size={16} />
|
||||
|
||||
@@ -116,6 +116,8 @@ const renderTokenKey = (
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
t,
|
||||
) => {
|
||||
const revealed = !!showKeys[record.id];
|
||||
const loading = !!loadingTokenKeys[record.id];
|
||||
@@ -145,18 +147,35 @@ const renderTokenKey = (
|
||||
await toggleTokenVisibility(record);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
theme='borderless'
|
||||
size='small'
|
||||
type='tertiary'
|
||||
icon={<IconCopy />}
|
||||
loading={loading}
|
||||
aria-label='copy token key'
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await copyTokenKey(record);
|
||||
}}
|
||||
/>
|
||||
<Dropdown
|
||||
trigger='click'
|
||||
position='bottomRight'
|
||||
clickToHide
|
||||
menu={[
|
||||
{
|
||||
node: 'item',
|
||||
name: t('复制密钥'),
|
||||
onClick: () => copyTokenKey(record),
|
||||
},
|
||||
{
|
||||
node: 'item',
|
||||
name: t('复制连接信息'),
|
||||
onClick: () => copyTokenConnectionString(record),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
theme='borderless'
|
||||
size='small'
|
||||
type='tertiary'
|
||||
icon={<IconCopy />}
|
||||
loading={loading}
|
||||
aria-label='copy token key'
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@@ -444,6 +463,7 @@ export const getTokensColumns = ({
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
@@ -484,6 +504,8 @@ export const getTokensColumns = ({
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
t,
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -43,6 +43,7 @@ const TokensTable = (tokensData) => {
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
@@ -60,6 +61,7 @@ const TokensTable = (tokensData) => {
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
@@ -73,6 +75,7 @@ const TokensTable = (tokensData) => {
|
||||
loadingTokenKeys,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
manageToken,
|
||||
onOpenLink,
|
||||
setEditingToken,
|
||||
|
||||
Vendored
+38
@@ -80,3 +80,41 @@ export function getServerAddress() {
|
||||
|
||||
return serverAddress;
|
||||
}
|
||||
|
||||
export const CHANNEL_CONN_CLIPBOARD_TYPE = 'newapi_channel_conn';
|
||||
|
||||
/**
|
||||
* @param {string} key - 完整的 API key(含 sk- 前缀)
|
||||
* @param {string} url - 服务器地址
|
||||
* @returns {string} JSON 格式的连接字符串
|
||||
*/
|
||||
export function encodeChannelConnectionString(key, url) {
|
||||
return JSON.stringify({
|
||||
_type: CHANNEL_CONN_CLIPBOARD_TYPE,
|
||||
key,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text - 剪贴板文本
|
||||
* @returns {{ key: string, url: string } | null}
|
||||
*/
|
||||
export function parseChannelConnectionString(text) {
|
||||
if (!text || typeof text !== 'string') return null;
|
||||
try {
|
||||
const parsed = JSON.parse(text.trim());
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === 'object' &&
|
||||
parsed._type === CHANNEL_CONN_CLIPBOARD_TYPE &&
|
||||
typeof parsed.key === 'string' &&
|
||||
typeof parsed.url === 'string'
|
||||
) {
|
||||
return { key: parsed.key, url: parsed.url };
|
||||
}
|
||||
} catch {
|
||||
// not valid JSON
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
+13
-1
@@ -29,7 +29,11 @@ import {
|
||||
} from '../../helpers';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
import { fetchTokenKey as fetchTokenKeyById } from '../../helpers/token';
|
||||
import {
|
||||
fetchTokenKey as fetchTokenKeyById,
|
||||
getServerAddress,
|
||||
encodeChannelConnectionString,
|
||||
} from '../../helpers/token';
|
||||
|
||||
export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -198,6 +202,13 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
await copyText(`sk-${fullKey}`);
|
||||
};
|
||||
|
||||
const copyTokenConnectionString = async (record) => {
|
||||
const fullKey = await fetchTokenKey(record);
|
||||
const serverUrl = getServerAddress();
|
||||
const connStr = encodeChannelConnectionString(`sk-${fullKey}`, serverUrl);
|
||||
await copyText(connStr);
|
||||
};
|
||||
|
||||
// Open link function for chat integrations
|
||||
const onOpenLink = async (type, url, record) => {
|
||||
const fullKey = await fetchTokenKey(record);
|
||||
@@ -465,6 +476,7 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
||||
fetchTokenKey,
|
||||
toggleTokenVisibility,
|
||||
copyTokenKey,
|
||||
copyTokenConnectionString,
|
||||
onOpenLink,
|
||||
manageToken,
|
||||
searchTokens,
|
||||
|
||||
Vendored
+12
-3
@@ -935,7 +935,7 @@
|
||||
"在此输入系统名称": "Enter the system name here",
|
||||
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "Enter privacy policy content here, supports Markdown & HTML code",
|
||||
"在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Enter the home page content here, supports Markdown",
|
||||
"域名IP过滤详细说明": "⚠️ This is an experimental option. A domain may resolve to multiple IPv4/IPv6 addresses. If enabled, ensure the IP filter list covers these addresses, otherwise access may fail.",
|
||||
"域名IP过滤详细说明": "Recommended: When enabled, domains are resolved via DNS and the resulting IPs are checked against private address ranges, effectively preventing DNS rebinding attacks that bypass SSRF protection. Note: A domain may resolve to multiple IPv4/IPv6 addresses. If you have configured an IP filter list, ensure it covers these addresses, otherwise access may fail.",
|
||||
"域名白名单": "Domain Whitelist",
|
||||
"域名黑名单": "Domain Blacklist",
|
||||
"基本信息": "Basic Information",
|
||||
@@ -1105,7 +1105,7 @@
|
||||
"密钥预览": "Key preview",
|
||||
"对于官方渠道,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",
|
||||
"对免费模型启用预消耗": "Enable pre-consumption for free models",
|
||||
"对域名启用 IP 过滤(实验性)": "Enable IP filtering for domains (experimental)",
|
||||
"对域名启用 IP 过滤(推荐开启)": "Enable IP filtering for domains (recommended)",
|
||||
"对外运营模式": "Default mode",
|
||||
"对象清理规则": "Object Pruning Rules",
|
||||
"导入": "Import",
|
||||
@@ -3352,6 +3352,15 @@
|
||||
"输出价格:{{symbol}}{{price}} / 1M tokens": "Output Price: {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "Output Price: {{symbol}}{{total}} / 1M tokens",
|
||||
"例如:gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$": "Example: gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$",
|
||||
"支持精确匹配;使用 regex: 开头可按正则匹配。": "Supports exact matching. Use a regex: prefix for regex matching."
|
||||
"支持精确匹配;使用 regex: 开头可按正则匹配。": "Supports exact matching. Use a regex: prefix for regex matching.",
|
||||
"复制密钥": "Copy Key",
|
||||
"复制连接信息": "Copy Connection String",
|
||||
"检测到剪贴板中的连接信息": "Connection info detected in clipboard",
|
||||
"自动填入": "Auto-fill",
|
||||
"忽略": "Ignore",
|
||||
"从剪贴板粘贴配置": "Paste Config",
|
||||
"剪贴板中未检测到连接信息": "No connection info found in clipboard",
|
||||
"连接信息已填入": "Connection info applied",
|
||||
"无法读取剪贴板": "Cannot read clipboard"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+12
-3
@@ -928,7 +928,7 @@
|
||||
"在此输入系统名称": "Saisissez le nom du système ici",
|
||||
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "Saisissez le contenu de la politique de confidentialité ici, prend en charge le code Markdown & HTML",
|
||||
"在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Saisissez le contenu de la page d'accueil ici, prend en charge Markdown & HTML. Après configuration, les informations d'état de la page d'accueil ne seront plus affichées. Si un lien est saisi, il sera utilisé comme attribut src de l'iframe, ce qui vous permet de définir n'importe quelle page web comme page d'accueil",
|
||||
"域名IP过滤详细说明": "⚠️ Il s'agit d'une option expérimentale. Un domaine peut se résoudre en plusieurs adresses IPv4/IPv6. Si cette option est activée, assurez-vous que la liste de filtres IP couvre ces adresses, sinon l'accès peut échouer.",
|
||||
"域名IP过滤详细说明": "Recommandé : lorsqu'il est activé, les domaines sont résolus par DNS et les IP résultantes sont vérifiées par rapport aux plages d'adresses privées, ce qui empêche efficacement les attaques de DNS rebinding qui contournent la protection SSRF. Remarque : un domaine peut se résoudre en plusieurs adresses IPv4/IPv6. Si vous avez configuré une liste de filtres IP, assurez-vous qu'elle couvre ces adresses, sinon l'accès peut échouer.",
|
||||
"域名白名单": "Liste blanche de domaines",
|
||||
"域名黑名单": "Liste noire de domaines",
|
||||
"基本信息": "Informations de base",
|
||||
@@ -1097,7 +1097,7 @@
|
||||
"密钥预览": "Aperçu de la clé",
|
||||
"对于官方渠道,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",
|
||||
"对免费模型启用预消耗": "Activer la préconsommation pour les modèles gratuits",
|
||||
"对域名启用 IP 过滤(实验性)": "Activer le filtrage IP pour les domaines (expérimental)",
|
||||
"对域名启用 IP 过滤(推荐开启)": "Activer le filtrage IP pour les domaines (recommandé)",
|
||||
"对外运营模式": "Mode par défaut",
|
||||
"对象清理规则": "Règles de nettoyage d'objets",
|
||||
"导入": "Importer",
|
||||
@@ -3308,6 +3308,15 @@
|
||||
"输入价格:{{symbol}}{{price}} / 1M tokens": "Prix d'entrée : {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格 {{symbol}}{{price}} / 1M tokens": "Prix de sortie {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{price}} / 1M tokens": "Prix de sortie : {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "Prix de sortie : {{symbol}}{{total}} / 1M tokens"
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "Prix de sortie : {{symbol}}{{total}} / 1M tokens",
|
||||
"复制密钥": "Copier la clé",
|
||||
"复制连接信息": "Copier les infos de connexion",
|
||||
"检测到剪贴板中的连接信息": "Informations de connexion détectées dans le presse-papiers",
|
||||
"自动填入": "Remplir auto",
|
||||
"忽略": "Ignorer",
|
||||
"从剪贴板粘贴配置": "Coller la config",
|
||||
"剪贴板中未检测到连接信息": "Aucune info de connexion trouvée dans le presse-papiers",
|
||||
"连接信息已填入": "Informations de connexion appliquées",
|
||||
"无法读取剪贴板": "Impossible de lire le presse-papiers"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+12
-3
@@ -919,7 +919,7 @@
|
||||
"在此输入系统名称": "システム名称を入力してください",
|
||||
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "プライバシーポリシーのコンテンツを入力してください。MarkdownとHTMLコードに対応しています",
|
||||
"在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "ホームのコンテンツを入力してください。MarkdownとHTMLに対応しています。設定後は、ホームのステータス情報が表示されなくなります。リンクを入力した場合は、そのリンクがiframeのsrc属性として使用され、任意のWebページをホームとして設定できます",
|
||||
"域名IP过滤详细说明": "ドメインIPフィルタリングの詳細説明",
|
||||
"域名IP过滤详细说明": "推奨:有効にすると、ドメインをDNS解決し、解決されたIPがプライベートアドレスかどうかを確認します。DNSリバインディング攻撃によるSSRF防護の回避を効果的に防止できます。注意:ドメインは複数のIPv4/IPv6アドレスに解決される場合があります。IPフィルタリストを設定している場合は、これらのアドレスをカバーしていることを確認してください。",
|
||||
"域名白名单": "ドメインホワイトリスト",
|
||||
"域名黑名单": "ドメインブラックリスト",
|
||||
"基本信息": "基本情報",
|
||||
@@ -1088,7 +1088,7 @@
|
||||
"密钥预览": "APIキーのプレビュー",
|
||||
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "公式チャネルの場合、new-apiにはベースURLが組み込まれているため、サードパーティのプロキシサイトやAzureの専用のエンドポイントでない限り、入力する必要はありません。",
|
||||
"对免费模型启用预消耗": "Enable pre-consumption for free models",
|
||||
"对域名启用 IP 过滤(实验性)": "ドメインのIPフィルタリングを有効にする(実験的)",
|
||||
"对域名启用 IP 过滤(推荐开启)": "ドメインのIPフィルタリングを有効にする(推奨)",
|
||||
"对外运营模式": "公開運用モード",
|
||||
"对象清理规则": "オブジェクトプルーニングルール",
|
||||
"导入": "インポート",
|
||||
@@ -3289,6 +3289,15 @@
|
||||
"输入价格:{{symbol}}{{price}} / 1M tokens": "入力価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格 {{symbol}}{{price}} / 1M tokens": "補完料金 {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{price}} / 1M tokens": "補完料金:{{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "補完料金:{{symbol}}{{total}} / 1M tokens"
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "補完料金:{{symbol}}{{total}} / 1M tokens",
|
||||
"复制密钥": "キーをコピー",
|
||||
"复制连接信息": "接続情報をコピー",
|
||||
"检测到剪贴板中的连接信息": "クリップボードに接続情報が検出されました",
|
||||
"自动填入": "自動入力",
|
||||
"忽略": "無視",
|
||||
"从剪贴板粘贴配置": "クリップボードから貼り付け",
|
||||
"剪贴板中未检测到连接信息": "クリップボードに接続情報が見つかりません",
|
||||
"连接信息已填入": "接続情報を入力しました",
|
||||
"无法读取剪贴板": "クリップボードを読み取れません"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+12
-3
@@ -934,7 +934,7 @@
|
||||
"在此输入系统名称": "Введите здесь название системы",
|
||||
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "Введите здесь содержимое политики конфиденциальности, поддерживается Markdown & HTML код",
|
||||
"在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Введите здесь содержание главной страницы, поддерживается код Markdown и HTML. После настройки информация о состоянии на главной странице больше не будет отображаться. Если введена ссылка, она будет использована как атрибут src для iframe, что позволяет установить любую веб-страницу как главную страницу",
|
||||
"域名IP过滤详细说明": "⚠️ Эта функция является экспериментальной опцией, доменное имя может быть разрешено в несколько адресов IPv4/IPv6, если включено, убедитесь, что список фильтрации IP покрывает эти адреса, иначе это может привести к сбою доступа.",
|
||||
"域名IP过滤详细说明": "Рекомендуется: при включении домены разрешаются через DNS, а полученные IP-адреса проверяются на принадлежность к частным диапазонам, что эффективно предотвращает атаки DNS rebinding, обходящие защиту SSRF. Примечание: домен может разрешаться в несколько адресов IPv4/IPv6. Если вы настроили список фильтрации IP, убедитесь, что он покрывает эти адреса, иначе доступ может быть нарушен.",
|
||||
"域名白名单": "Белый список доменов",
|
||||
"域名黑名单": "Чёрный список доменов",
|
||||
"基本信息": "Основная информация",
|
||||
@@ -1103,7 +1103,7 @@
|
||||
"密钥预览": "Предпросмотр ключа",
|
||||
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "Для официальных каналов new-api уже имеет встроенные адреса, если это не сторонние прокси-сайты или специальные адреса доступа Azure, заполнять не нужно",
|
||||
"对免费模型启用预消耗": "Включить предварительное списание для бесплатных моделей",
|
||||
"对域名启用 IP 过滤(实验性)": "Включить IP-фильтрацию для доменов (экспериментально)",
|
||||
"对域名启用 IP 过滤(推荐开启)": "Включить IP-фильтрацию для доменов (рекомендуется)",
|
||||
"对外运营模式": "Режим внешней эксплуатации",
|
||||
"对象清理规则": "Правила очистки объектов",
|
||||
"导入": "Импорт",
|
||||
@@ -3322,6 +3322,15 @@
|
||||
"输入价格:{{symbol}}{{price}} / 1M tokens": "Цена ввода: {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格 {{symbol}}{{price}} / 1M tokens": "Цена вывода {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{price}} / 1M tokens": "Цена вывода: {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "Цена вывода: {{symbol}}{{total}} / 1M tokens"
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "Цена вывода: {{symbol}}{{total}} / 1M tokens",
|
||||
"复制密钥": "Копировать ключ",
|
||||
"复制连接信息": "Копировать данные подключения",
|
||||
"检测到剪贴板中的连接信息": "В буфере обмена обнаружены данные подключения",
|
||||
"自动填入": "Заполнить",
|
||||
"忽略": "Игнорировать",
|
||||
"从剪贴板粘贴配置": "Вставить конфигурацию",
|
||||
"剪贴板中未检测到连接信息": "Данные подключения не найдены в буфере обмена",
|
||||
"连接信息已填入": "Данные подключения применены",
|
||||
"无法读取剪贴板": "Не удалось прочитать буфер обмена"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+12
-3
@@ -920,7 +920,7 @@
|
||||
"在此输入系统名称": "Nhập tên hệ thống tại đây",
|
||||
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "Nhập nội dung chính sách bảo mật tại đây, hỗ trợ mã Markdown & HTML",
|
||||
"在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Nhập nội dung trang chủ tại đây, hỗ trợ Markdown",
|
||||
"域名IP过滤详细说明": "⚠️ Đây là tùy chọn thử nghiệm. Một tên miền có thể phân giải thành nhiều địa chỉ IPv4/IPv6. Nếu bật, hãy đảm bảo danh sách lọc IP bao gồm các địa chỉ này, nếu không truy cập có thể thất bại.",
|
||||
"域名IP过滤详细说明": "Khuyến nghị: Khi được bật, tên miền sẽ được phân giải DNS và các IP kết quả sẽ được kiểm tra xem có thuộc dải địa chỉ riêng tư hay không, ngăn chặn hiệu quả các cuộc tấn công DNS rebinding vượt qua bảo vệ SSRF. Lưu ý: Một tên miền có thể phân giải thành nhiều địa chỉ IPv4/IPv6. Nếu bạn đã cấu hình danh sách lọc IP, hãy đảm bảo nó bao gồm các địa chỉ này, nếu không truy cập có thể thất bại.",
|
||||
"域名白名单": "Danh sách trắng tên miền",
|
||||
"域名黑名单": "Danh sách đen tên miền",
|
||||
"基本信息": "Thông tin cơ bản",
|
||||
@@ -1089,7 +1089,7 @@
|
||||
"密钥预览": "Xem trước khóa",
|
||||
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "Đối với các kênh chính thức, new-api đã tích hợp sẵn địa chỉ. Trừ khi đó là trang web proxy của bên thứ ba hoặc địa chỉ truy cập đặc biệt của Azure, không cần điền vào",
|
||||
"对免费模型启用预消耗": "Enable pre-consumption for free models",
|
||||
"对域名启用 IP 过滤(实验性)": "Bật lọc IP cho tên miền (thử nghiệm)",
|
||||
"对域名启用 IP 过滤(推荐开启)": "Bật lọc IP cho tên miền (khuyến nghị)",
|
||||
"对外运营模式": "Chế độ mặc định",
|
||||
"对象清理规则": "Quy tắc dọn dẹp đối tượng",
|
||||
"导入": "Nhập",
|
||||
@@ -3859,6 +3859,15 @@
|
||||
"补全倍率 {{completionRatio}}": "Tỷ lệ hoàn thành {{completionRatio}}",
|
||||
"输出价格 {{symbol}}{{price}} / 1M tokens": "Giá đầu ra {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{price}} / 1M tokens": "Giá đầu ra: {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "Giá đầu ra: {{symbol}}{{total}} / 1M tokens"
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "Giá đầu ra: {{symbol}}{{total}} / 1M tokens",
|
||||
"复制密钥": "Sao chép khóa",
|
||||
"复制连接信息": "Sao chép thông tin kết nối",
|
||||
"检测到剪贴板中的连接信息": "Phát hiện thông tin kết nối trong bộ nhớ tạm",
|
||||
"自动填入": "Tự động điền",
|
||||
"忽略": "Bỏ qua",
|
||||
"从剪贴板粘贴配置": "Dán cấu hình",
|
||||
"剪贴板中未检测到连接信息": "Không tìm thấy thông tin kết nối trong bộ nhớ tạm",
|
||||
"连接信息已填入": "Đã áp dụng thông tin kết nối",
|
||||
"无法读取剪贴板": "Không thể đọc bộ nhớ tạm"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+12
-3
@@ -735,7 +735,7 @@
|
||||
"在此输入系统名称": "在此输入系统名称",
|
||||
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "在此输入隐私政策内容,支持 Markdown & HTML 代码",
|
||||
"在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页",
|
||||
"域名IP过滤详细说明": "⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。",
|
||||
"域名IP过滤详细说明": "推荐开启:开启后会对域名进行 DNS 解析并检查解析后的 IP 是否为私有地址,可有效防止 DNS 重绑定攻击绕过 SSRF 防护。注意:域名可能解析到多个 IPv4/IPv6 地址,若配置了 IP 过滤列表,请确保覆盖这些地址,否则可能导致访问失败。",
|
||||
"域名白名单": "域名白名单",
|
||||
"域名黑名单": "域名黑名单",
|
||||
"基本信息": "基本信息",
|
||||
@@ -870,7 +870,7 @@
|
||||
"密钥预览": "密钥预览",
|
||||
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写",
|
||||
"对免费模型启用预消耗": "对免费模型启用预消耗",
|
||||
"对域名启用 IP 过滤(实验性)": "对域名启用 IP 过滤(实验性)",
|
||||
"对域名启用 IP 过滤(推荐开启)": "对域名启用 IP 过滤(推荐开启)",
|
||||
"对外运营模式": "对外运营模式",
|
||||
"导入": "导入",
|
||||
"导入的配置将覆盖当前设置,是否继续?": "导入的配置将覆盖当前设置,是否继续?",
|
||||
@@ -2956,6 +2956,15 @@
|
||||
"输入价格:{{symbol}}{{price}} / 1M tokens": "输入价格:{{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格 {{symbol}}{{price}} / 1M tokens": "输出价格 {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{price}} / 1M tokens": "输出价格:{{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "输出价格:{{symbol}}{{total}} / 1M tokens"
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "输出价格:{{symbol}}{{total}} / 1M tokens",
|
||||
"复制密钥": "复制密钥",
|
||||
"复制连接信息": "复制连接信息",
|
||||
"检测到剪贴板中的连接信息": "检测到剪贴板中的连接信息",
|
||||
"自动填入": "自动填入",
|
||||
"忽略": "忽略",
|
||||
"从剪贴板粘贴配置": "从剪贴板粘贴配置",
|
||||
"剪贴板中未检测到连接信息": "剪贴板中未检测到连接信息",
|
||||
"连接信息已填入": "连接信息已填入",
|
||||
"无法读取剪贴板": "无法读取剪贴板"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+12
-3
@@ -737,7 +737,7 @@
|
||||
"在此输入系统名称": "在此輸入系統名稱",
|
||||
"在此输入隐私政策内容,支持 Markdown & HTML 代码": "在此輸入隱私政策內容,支援 Markdown & HTML 程式碼",
|
||||
"在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "在此輸入首頁內容,支援 Markdown & HTML 程式碼,設定後首頁的狀態訊息將不再顯示。如果輸入的是一個連結,則會使用該連結作為 iframe 的 src 屬性,這允許你設定任意網頁作為首頁",
|
||||
"域名IP过滤详细说明": "⚠️此功能為實驗性選項,域名可能解析到多個 IPv4/IPv6 位址,若開啟,請確保 IP 過濾列表覆蓋這些位址,否則可能導致訪問失敗。",
|
||||
"域名IP过滤详细说明": "建議開啟:開啟後會對域名進行 DNS 解析並檢查解析後的 IP 是否為私有位址,可有效防止 DNS 重綁定攻擊繞過 SSRF 防護。注意:域名可能解析到多個 IPv4/IPv6 位址,若配置了 IP 過濾列表,請確保覆蓋這些位址,否則可能導致訪問失敗。",
|
||||
"域名白名单": "域名白名單",
|
||||
"域名黑名单": "域名黑名單",
|
||||
"基本信息": "基本資訊",
|
||||
@@ -873,7 +873,7 @@
|
||||
"密钥预览": "密鑰預覽",
|
||||
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "對於官方管道,new-api已經內置位址,除非是第三方代理站點或者Azure的特殊接入位址,否則不需要填寫",
|
||||
"对免费模型启用预消耗": "對免費模型啟用預消耗",
|
||||
"对域名启用 IP 过滤(实验性)": "對域名啟用 IP 過濾(實驗性)",
|
||||
"对域名启用 IP 过滤(推荐开启)": "對域名啟用 IP 過濾(建議開啟)",
|
||||
"对外运营模式": "對外運營模式",
|
||||
"导入": "導入",
|
||||
"导入的配置将覆盖当前设置,是否继续?": "導入的設定將覆蓋當前設定,是否繼續?",
|
||||
@@ -2973,6 +2973,15 @@
|
||||
"输入价格:{{symbol}}{{price}} / 1M tokens": "輸入價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格 {{symbol}}{{price}} / 1M tokens": "輸出價格 {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{price}} / 1M tokens": "輸出價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "輸出價格:{{symbol}}{{total}} / 1M tokens"
|
||||
"输出价格:{{symbol}}{{total}} / 1M tokens": "輸出價格:{{symbol}}{{total}} / 1M tokens",
|
||||
"复制密钥": "複製金鑰",
|
||||
"复制连接信息": "複製連線資訊",
|
||||
"检测到剪贴板中的连接信息": "偵測到剪貼簿中的連線資訊",
|
||||
"自动填入": "自動填入",
|
||||
"忽略": "忽略",
|
||||
"从剪贴板粘贴配置": "從剪貼簿貼上設定",
|
||||
"剪贴板中未检测到连接信息": "剪貼簿中未偵測到連線資訊",
|
||||
"连接信息已填入": "連線資訊已填入",
|
||||
"无法读取剪贴板": "無法讀取剪貼簿"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+12
@@ -31,6 +31,13 @@ body {
|
||||
background-color: var(--semi-color-bg-0);
|
||||
}
|
||||
|
||||
/* 桌面端禁止 body 纵向滚动 - 防止 VChart tooltip 触发页面滚动条 */
|
||||
@media (min-width: 768px) {
|
||||
body {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
@@ -469,6 +476,9 @@ html.dark .sbg-variant-green {
|
||||
.custom-footer {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.custom-footer.na-cb6feafeb3990c78 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 卡片内容容器通用样式 */
|
||||
.card-content-container {
|
||||
@@ -994,3 +1004,5 @@ html.dark .with-pastel-balls::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ec-dbcd0a3c01b55203 { forced-color-adjust: auto; }
|
||||
|
||||
Reference in New Issue
Block a user