Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cad6b9d7f | |||
| 8aaec8b1cc | |||
| b2a40d3381 | |||
| bf130c5cde | |||
| f7adf02eb4 | |||
| d0c2d2c6fb | |||
| ee7cedd577 | |||
| 8c8661d0d7 | |||
| d15e14b117 | |||
| 3ab65a8221 | |||
| 7cfaf6c335 | |||
| 2bedd31b42 | |||
| c20060931b | |||
| 8b22161527 | |||
| 3d0ac2d049 | |||
| b81d3427ee | |||
| b4df9955f4 | |||
| 59c582d13c | |||
| 2819e3a1d1 | |||
| ed7f839911 | |||
| 040e8c1da8 | |||
| 0664bb3f65 | |||
| c7cf20391e | |||
| b07f0b9626 | |||
| 53cf37a469 | |||
| 3bda738ec1 | |||
| 160cb28572 | |||
| 274307b0a9 | |||
| a19a63b98c | |||
| 78e4cb3cad | |||
| c734db34e8 | |||
| a18ea3cc16 | |||
| aafbd78887 | |||
| 77897a8101 |
@@ -0,0 +1,28 @@
|
||||
# ⚠️ 提交说明 / PR Notice
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - 请提供**人工撰写**的简洁摘要,避免直接粘贴未经整理的 AI 输出。
|
||||
|
||||
## 📝 变更描述 / Description
|
||||
(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)
|
||||
|
||||
## 🚀 变更类型 / Type of change
|
||||
- [ ] 🐛 Bug 修复 (Bug fix) - *请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug*
|
||||
- [ ] ✨ 新功能 (New feature) - *重大特性建议先通过 Issue 沟通*
|
||||
- [ ] ⚡ 性能优化 / 重构 (Refactor)
|
||||
- [ ] 📝 文档更新 (Documentation)
|
||||
|
||||
## 🔗 关联任务 / Related Issue
|
||||
- Closes # (如有)
|
||||
|
||||
## ✅ 提交前检查项 / Checklist
|
||||
- [ ] **人工确认:** 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
|
||||
- [ ] **非重复提交:** 我已搜索现有的 [Issues](https://github.com/QuantumNous/new-api/issues) 与 [PRs](https://github.com/QuantumNous/new-api/pulls),确认不是重复提交。
|
||||
- [ ] **Bug fix 说明:** 若此 PR 标记为 `Bug fix`,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
|
||||
- [ ] **变更理解:** 我已理解这些更改的工作原理及可能影响。
|
||||
- [ ] **范围聚焦:** 本 PR 未包含任何与当前任务无关的代码改动。
|
||||
- [ ] **本地验证:** 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
|
||||
- [ ] **安全合规:** 代码中无敏感凭据,且符合项目代码规范。
|
||||
|
||||
## 📸 运行证明 / Proof of Work
|
||||
(请在此粘贴截图、关键日志或测试报告,以证明变更生效)
|
||||
@@ -1,29 +0,0 @@
|
||||
# ⚠️ 提交警告 / PR Warning
|
||||
> **请注意:** 请提供**人工撰写**的简洁摘要。包含大量 AI 灌水内容、逻辑混乱或无视模版的 PR **可能会被无视或直接关闭**。
|
||||
|
||||
---
|
||||
|
||||
## 💡 沟通提示 / Pre-submission
|
||||
> **重大功能变更?** 请先提交 Issue 交流,避免无效劳动。
|
||||
|
||||
## 📝 变更描述 / Description
|
||||
(简述:做了什么?为什么这样改能生效?你必须理解代码逻辑,禁止粘贴 AI 废话)
|
||||
|
||||
## 🚀 变更类型 / Type of change
|
||||
- [ ] 🐛 Bug 修复 (Bug fix)
|
||||
- [ ] ✨ 新功能 (New feature) - *重大特性建议先 Issue 沟通*
|
||||
- [ ] ⚡ 性能优化 / 重构 (Refactor)
|
||||
- [ ] 📝 文档更新 (Documentation)
|
||||
|
||||
## 🔗 关联任务 / Related Issue
|
||||
- Closes # (如有)
|
||||
|
||||
## ✅ 提交前检查项 / Checklist
|
||||
- [ ] **人工确认:** 我已亲自撰写此描述,去除了 AI 原始输出的冗余。
|
||||
- [ ] **深度理解:** 我已**完全理解**这些更改的工作原理及潜在影响。
|
||||
- [ ] **范围聚焦:** 本 PR 未包含任何与当前任务无关的代码改动。
|
||||
- [ ] **本地验证:** 已在本地运行并通过了测试或手动验证。
|
||||
- [ ] **安全合规:** 代码中无敏感凭据,且符合项目代码规范。
|
||||
|
||||
## 📸 运行证明 / Proof of Work
|
||||
(请在此粘贴截图、关键日志或测试报告,以证明变更生效)
|
||||
@@ -0,0 +1,33 @@
|
||||
name: PR Check
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
pr-quality:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peakoss/anti-slop@v0.2.1
|
||||
with:
|
||||
max-failures: 4
|
||||
require-description: true
|
||||
|
||||
# require-linked-issue: false
|
||||
blocked-terms: |
|
||||
🤖 Generated with Claude Code
|
||||
|
||||
require-pr-template: true
|
||||
strict-pr-template-sections: "✅ 提交前检查项 / Checklist"
|
||||
|
||||
detect-spam-usernames: true
|
||||
min-account-age: 30
|
||||
|
||||
failure-add-pr-labels: "pr-check-failed"
|
||||
failure-pr-message: "感谢您的提交。由于该 PR 未遵循我们的贡献模板,且被识别为缺乏人工参与的纯 AI 生成内容 (AI Slop),我们将先予以关闭。我们更欢迎经过人工审核、验证并带有个人思考的贡献。如果您认为这其中存在误解,请回复告知。/ Thank you for your submission. This PR has been closed because it does not follow our contribution template and has been identified as purely AI-generated content (AI Slop) without meaningful human involvement. We prioritize contributions that are human-verified and reflect individual effort. If you believe this is a mistake, please let us know by replying to this comment."
|
||||
close-pr: true
|
||||
@@ -29,3 +29,5 @@ data/
|
||||
.gomodcache/
|
||||
.gocache-temp
|
||||
.gopath
|
||||
|
||||
token_estimator_test.go
|
||||
@@ -80,6 +80,7 @@ var InsecureTLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
var SMTPServer = ""
|
||||
var SMTPPort = 587
|
||||
var SMTPSSLEnabled = false
|
||||
var SMTPForceAuthLogin = false
|
||||
var SMTPAccount = ""
|
||||
var SMTPFrom = ""
|
||||
var SMTPToken = ""
|
||||
|
||||
+15
-4
@@ -19,6 +19,20 @@ func generateMessageID() (string, error) {
|
||||
return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain), nil
|
||||
}
|
||||
|
||||
func shouldUseSMTPLoginAuth() bool {
|
||||
if SMTPForceAuthLogin {
|
||||
return true
|
||||
}
|
||||
return isOutlookServer(SMTPAccount) || slices.Contains(EmailLoginAuthServerList, SMTPServer)
|
||||
}
|
||||
|
||||
func getSMTPAuth() smtp.Auth {
|
||||
if shouldUseSMTPLoginAuth() {
|
||||
return LoginAuth(SMTPAccount, SMTPToken)
|
||||
}
|
||||
return smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
|
||||
}
|
||||
|
||||
func SendEmail(subject string, receiver string, content string) error {
|
||||
if SMTPFrom == "" { // for compatibility
|
||||
SMTPFrom = SMTPAccount
|
||||
@@ -38,7 +52,7 @@ func SendEmail(subject string, receiver string, content string) error {
|
||||
"Message-ID: %s\r\n"+ // 添加 Message-ID 头
|
||||
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
||||
receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), id, content))
|
||||
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
|
||||
auth := getSMTPAuth()
|
||||
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
||||
to := strings.Split(receiver, ";")
|
||||
var err error
|
||||
@@ -80,9 +94,6 @@ func SendEmail(subject string, receiver string, content string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if isOutlookServer(SMTPAccount) || slices.Contains(EmailLoginAuthServerList, SMTPServer) {
|
||||
auth = LoginAuth(SMTPAccount, SMTPToken)
|
||||
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
|
||||
} else {
|
||||
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
|
||||
}
|
||||
|
||||
@@ -65,4 +65,5 @@ const (
|
||||
|
||||
// ContextKeyLanguage stores the user's language preference for i18n
|
||||
ContextKeyLanguage ContextKey = "language"
|
||||
ContextKeyIsStream ContextKey = "is_stream"
|
||||
)
|
||||
|
||||
@@ -150,6 +150,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
|
||||
}
|
||||
}
|
||||
cache.WriteContext(c)
|
||||
c.Set("id", 1)
|
||||
|
||||
//c.Request.Header.Set("Authorization", "Bearer "+channel.Key)
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
@@ -274,7 +275,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
|
||||
return testResult{
|
||||
context: c,
|
||||
localErr: err,
|
||||
newAPIError: types.NewError(err, types.ErrorCodeModelPriceError),
|
||||
newAPIError: types.NewError(err, types.ErrorCodeModelPriceError, types.ErrOptionWithStatusCode(http.StatusBadRequest)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,11 +757,15 @@ func TestChannel(c *gin.Context) {
|
||||
tik := time.Now()
|
||||
result := testChannel(channel, testModel, endpointType, isStream)
|
||||
if result.localErr != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
resp := gin.H{
|
||||
"success": false,
|
||||
"message": result.localErr.Error(),
|
||||
"time": 0.0,
|
||||
})
|
||||
}
|
||||
if result.newAPIError != nil {
|
||||
resp["error_code"] = result.newAPIError.GetErrorCode()
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
tok := time.Now()
|
||||
@@ -769,9 +774,10 @@ func TestChannel(c *gin.Context) {
|
||||
consumedTime := float64(milliseconds) / 1000.0
|
||||
if result.newAPIError != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": result.newAPIError.Error(),
|
||||
"time": consumedTime,
|
||||
"success": false,
|
||||
"message": result.newAPIError.Error(),
|
||||
"time": consumedTime,
|
||||
"error_code": result.newAPIError.GetErrorCode(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
+2
-2
@@ -151,7 +151,7 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
|
||||
|
||||
priceData, err := helper.ModelPriceHelper(c, relayInfo, tokens, meta)
|
||||
if err != nil {
|
||||
newAPIError = types.NewError(err, types.ErrorCodeModelPriceError)
|
||||
newAPIError = types.NewError(err, types.ErrorCodeModelPriceError, types.ErrOptionWithStatusCode(http.StatusBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t
|
||||
startTime = time.Now()
|
||||
}
|
||||
useTimeSeconds := int(time.Since(startTime).Seconds())
|
||||
model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.MaskSensitiveErrorWithStatusCode(), tokenId, useTimeSeconds, false, userGroup, other)
|
||||
model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.MaskSensitiveErrorWithStatusCode(), tokenId, useTimeSeconds, common.GetContextKeyBool(c, constant.ContextKeyIsStream), userGroup, other)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -340,6 +340,10 @@ func EpayNotify(c *gin.Context) {
|
||||
log.Printf("易支付回调未找到订单: %v", verifyInfo)
|
||||
return
|
||||
}
|
||||
if topUp.PaymentMethod == "stripe" || topUp.PaymentMethod == "creem" || topUp.PaymentMethod == "waffo" {
|
||||
log.Printf("易支付回调订单支付方式不匹配: %s, 订单号: %s", topUp.PaymentMethod, verifyInfo.ServiceTradeNo)
|
||||
return
|
||||
}
|
||||
if topUp.Status == "pending" {
|
||||
topUp.Status = "success"
|
||||
err := topUp.Update()
|
||||
|
||||
@@ -108,12 +108,13 @@ func (*CreemAdaptor) RequestPay(c *gin.Context, req *CreemPayRequest) {
|
||||
|
||||
// 先创建订单记录,使用产品配置的金额和充值额度
|
||||
topUp := &model.TopUp{
|
||||
UserId: id,
|
||||
Amount: selectedProduct.Quota, // 充值额度
|
||||
Money: selectedProduct.Price, // 支付金额
|
||||
TradeNo: referenceId,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
UserId: id,
|
||||
Amount: selectedProduct.Quota, // 充值额度
|
||||
Money: selectedProduct.Price, // 支付金额
|
||||
TradeNo: referenceId,
|
||||
PaymentMethod: PaymentMethodCreem,
|
||||
CreateTime: time.Now().Unix(),
|
||||
Status: common.TopUpStatusPending,
|
||||
}
|
||||
err = topUp.Insert()
|
||||
if err != nil {
|
||||
|
||||
@@ -146,6 +146,12 @@ func RequestStripePay(c *gin.Context) {
|
||||
}
|
||||
|
||||
func StripeWebhook(c *gin.Context) {
|
||||
if setting.StripeWebhookSecret == "" {
|
||||
log.Println("Stripe Webhook Secret 未配置,拒绝处理")
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
log.Printf("解析Stripe Webhook参数失败: %v\n", err)
|
||||
@@ -154,8 +160,7 @@ func StripeWebhook(c *gin.Context) {
|
||||
}
|
||||
|
||||
signature := c.GetHeader("Stripe-Signature")
|
||||
endpointSecret := setting.StripeWebhookSecret
|
||||
event, err := webhook.ConstructEventWithOptions(payload, signature, endpointSecret, webhook.ConstructEventOptions{
|
||||
event, err := webhook.ConstructEventWithOptions(payload, signature, setting.StripeWebhookSecret, webhook.ConstructEventOptions{
|
||||
IgnoreAPIVersionMismatch: true,
|
||||
})
|
||||
|
||||
@@ -170,6 +175,10 @@ func StripeWebhook(c *gin.Context) {
|
||||
sessionCompleted(event)
|
||||
case stripe.EventTypeCheckoutSessionExpired:
|
||||
sessionExpired(event)
|
||||
case stripe.EventTypeCheckoutSessionAsyncPaymentSucceeded:
|
||||
sessionAsyncPaymentSucceeded(event)
|
||||
case stripe.EventTypeCheckoutSessionAsyncPaymentFailed:
|
||||
sessionAsyncPaymentFailed(event)
|
||||
default:
|
||||
log.Printf("不支持的Stripe Webhook事件类型: %s\n", event.Type)
|
||||
}
|
||||
@@ -186,7 +195,70 @@ func sessionCompleted(event stripe.Event) {
|
||||
return
|
||||
}
|
||||
|
||||
// Try complete subscription order first
|
||||
paymentStatus := event.GetObjectValue("payment_status")
|
||||
if paymentStatus != "paid" {
|
||||
log.Printf("Stripe Checkout 支付尚未完成,payment_status: %s, ref: %s(等待异步支付结果)", paymentStatus, referenceId)
|
||||
return
|
||||
}
|
||||
|
||||
fulfillOrder(event, referenceId, customerId)
|
||||
}
|
||||
|
||||
// sessionAsyncPaymentSucceeded handles delayed payment methods (bank transfer, SEPA, etc.)
|
||||
// that confirm payment after the checkout session completes.
|
||||
func sessionAsyncPaymentSucceeded(event stripe.Event) {
|
||||
customerId := event.GetObjectValue("customer")
|
||||
referenceId := event.GetObjectValue("client_reference_id")
|
||||
log.Printf("Stripe 异步支付成功: %s", referenceId)
|
||||
|
||||
fulfillOrder(event, referenceId, customerId)
|
||||
}
|
||||
|
||||
// sessionAsyncPaymentFailed marks orders as failed when delayed payment methods
|
||||
// ultimately fail (e.g. bank transfer not received, SEPA rejected).
|
||||
func sessionAsyncPaymentFailed(event stripe.Event) {
|
||||
referenceId := event.GetObjectValue("client_reference_id")
|
||||
log.Printf("Stripe 异步支付失败: %s", referenceId)
|
||||
|
||||
if len(referenceId) == 0 {
|
||||
log.Println("异步支付失败事件未提供支付单号")
|
||||
return
|
||||
}
|
||||
|
||||
LockOrder(referenceId)
|
||||
defer UnlockOrder(referenceId)
|
||||
|
||||
topUp := model.GetTopUpByTradeNo(referenceId)
|
||||
if topUp == nil {
|
||||
log.Println("异步支付失败,充值订单不存在:", referenceId)
|
||||
return
|
||||
}
|
||||
|
||||
if topUp.PaymentMethod != PaymentMethodStripe {
|
||||
log.Printf("异步支付失败,订单支付方式不匹配: %s, ref: %s", topUp.PaymentMethod, referenceId)
|
||||
return
|
||||
}
|
||||
|
||||
if topUp.Status != common.TopUpStatusPending {
|
||||
log.Printf("异步支付失败,订单状态非pending: %s, ref: %s", topUp.Status, referenceId)
|
||||
return
|
||||
}
|
||||
|
||||
topUp.Status = common.TopUpStatusFailed
|
||||
if err := topUp.Update(); err != nil {
|
||||
log.Printf("标记充值订单失败出错: %v, ref: %s", err, referenceId)
|
||||
return
|
||||
}
|
||||
log.Printf("充值订单已标记为失败: %s", referenceId)
|
||||
}
|
||||
|
||||
// fulfillOrder is the shared logic for crediting quota after payment is confirmed.
|
||||
func fulfillOrder(event stripe.Event, referenceId string, customerId string) {
|
||||
if len(referenceId) == 0 {
|
||||
log.Println("未提供支付单号")
|
||||
return
|
||||
}
|
||||
|
||||
LockOrder(referenceId)
|
||||
defer UnlockOrder(referenceId)
|
||||
payload := map[string]any{
|
||||
|
||||
+53
-7
@@ -52,10 +52,15 @@ func Login(c *gin.Context) {
|
||||
}
|
||||
err = user.ValidateAndFill()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": err.Error(),
|
||||
"success": false,
|
||||
})
|
||||
switch {
|
||||
case errors.Is(err, model.ErrDatabase):
|
||||
common.SysLog(fmt.Sprintf("Login database error for user %s: %v", username, err))
|
||||
common.ApiErrorI18n(c, i18n.MsgDatabaseError)
|
||||
case errors.Is(err, model.ErrUserEmptyCredentials):
|
||||
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
|
||||
default:
|
||||
common.ApiErrorI18n(c, i18n.MsgUserUsernameOrPasswordError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -572,9 +577,6 @@ func UpdateUser(c *gin.Context) {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if originUser.Quota != updatedUser.Quota {
|
||||
model.RecordLog(originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", logger.LogQuota(originUser.Quota), logger.LogQuota(updatedUser.Quota)))
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
@@ -841,6 +843,8 @@ func CreateUser(c *gin.Context) {
|
||||
type ManageRequest struct {
|
||||
Id int `json:"id"`
|
||||
Action string `json:"action"`
|
||||
Value int `json:"value"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
// ManageUser Only admin user can do this
|
||||
@@ -907,6 +911,48 @@ func ManageUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user.Role = common.RoleCommonUser
|
||||
case "add_quota":
|
||||
adminName := c.GetString("username")
|
||||
switch req.Mode {
|
||||
case "add":
|
||||
if req.Value <= 0 {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserQuotaChangeZero)
|
||||
return
|
||||
}
|
||||
if err := model.IncreaseUserQuota(user.Id, req.Value, true); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
model.RecordLog(user.Id, model.LogTypeManage,
|
||||
fmt.Sprintf("管理员(%s)增加用户额度 %s", adminName, logger.LogQuota(req.Value)))
|
||||
case "subtract":
|
||||
if req.Value <= 0 {
|
||||
common.ApiErrorI18n(c, i18n.MsgUserQuotaChangeZero)
|
||||
return
|
||||
}
|
||||
if err := model.DecreaseUserQuota(user.Id, req.Value, true); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
model.RecordLog(user.Id, model.LogTypeManage,
|
||||
fmt.Sprintf("管理员(%s)减少用户额度 %s", adminName, logger.LogQuota(req.Value)))
|
||||
case "override":
|
||||
oldQuota := user.Quota
|
||||
if err := model.DB.Model(&model.User{}).Where("id = ?", user.Id).Update("quota", req.Value).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
model.RecordLog(user.Id, model.LogTypeManage,
|
||||
fmt.Sprintf("管理员(%s)覆盖用户额度从 %s 为 %s", adminName, logger.LogQuota(oldQuota), logger.LogQuota(req.Value)))
|
||||
default:
|
||||
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.Update(false); err != nil {
|
||||
|
||||
+53
-1
@@ -3281,6 +3281,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"cache_control": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"inference_geo": {
|
||||
"type": "string"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
@@ -3333,7 +3340,8 @@
|
||||
"enum": [
|
||||
"auto",
|
||||
"any",
|
||||
"tool"
|
||||
"tool",
|
||||
"none"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
@@ -3358,6 +3366,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"context_management": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"output_config": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"output_format": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
},
|
||||
"container": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"mcp_servers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -3365,6 +3403,20 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"speed": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"standard",
|
||||
"fast"
|
||||
]
|
||||
},
|
||||
"service_tier": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"auto",
|
||||
"standard_only"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,6 +18,16 @@ type AudioRequest struct {
|
||||
Speed *float64 `json:"speed,omitempty"`
|
||||
StreamFormat string `json:"stream_format,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
// vllm-omini
|
||||
TaskType json.RawMessage `json:"task_type,omitempty"`
|
||||
Language json.RawMessage `json:"language,omitempty"`
|
||||
RefAudio json.RawMessage `json:"ref_audio,omitempty"`
|
||||
RefText json.RawMessage `json:"ref_text,omitempty"`
|
||||
XVectorOnlyMode json.RawMessage `json:"x_vector_only_mode,omitempty"`
|
||||
MaxNewTokens json.RawMessage `json:"max_new_tokens,omitempty"`
|
||||
InitialCodecChunkFrames json.RawMessage `json:"initial_codec_chunk_frames,omitempty"`
|
||||
// TODO:ensure that the logic remains correct after the stream is started.
|
||||
//Stream json.RawMessage `json:"stream,omitempty"`
|
||||
}
|
||||
|
||||
func (r *AudioRequest) GetTokenCountMeta() *types.TokenCountMeta {
|
||||
|
||||
@@ -30,6 +30,7 @@ type ChannelOtherSettings struct {
|
||||
ClaudeBetaQuery bool `json:"claude_beta_query,omitempty"` // Claude 渠道是否强制追加 ?beta=true
|
||||
AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费)
|
||||
AllowInferenceGeo bool `json:"allow_inference_geo,omitempty"` // 是否允许 inference_geo 透传(仅 Claude,默认过滤以满足数据驻留合规
|
||||
AllowSpeed bool `json:"allow_speed,omitempty"` // 是否允许 speed 透传(仅 Claude,默认过滤以避免意外切换推理速度模式)
|
||||
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
|
||||
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
|
||||
AllowIncludeObfuscation bool `json:"allow_include_obfuscation,omitempty"` // 是否允许 stream_options.include_obfuscation 透传(默认过滤以避免关闭流混淆保护)
|
||||
|
||||
+8
-4
@@ -204,10 +204,11 @@ type ClaudeToolChoice struct {
|
||||
}
|
||||
|
||||
type ClaudeRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
System any `json:"system,omitempty"`
|
||||
Messages []ClaudeMessage `json:"messages,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
System any `json:"system,omitempty"`
|
||||
Messages []ClaudeMessage `json:"messages,omitempty"`
|
||||
CacheControl json.RawMessage `json:"cache_control,omitempty"`
|
||||
// InferenceGeo controls Claude data residency region.
|
||||
// This field is filtered by default and can be enabled via channel setting allow_inference_geo.
|
||||
InferenceGeo string `json:"inference_geo,omitempty"`
|
||||
@@ -227,6 +228,9 @@ type ClaudeRequest struct {
|
||||
Thinking *Thinking `json:"thinking,omitempty"`
|
||||
McpServers json.RawMessage `json:"mcp_servers,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
// Speed specifies the Claude inference speed mode.
|
||||
// This field is filtered by default and can be enabled via channel setting allow_speed.
|
||||
Speed json.RawMessage `json:"speed,omitempty"`
|
||||
// ServiceTier specifies upstream service level and may affect billing.
|
||||
// This field is filtered by default and can be enabled via channel setting allow_service_tier.
|
||||
ServiceTier string `json:"service_tier,omitempty"`
|
||||
|
||||
@@ -272,7 +272,7 @@ type OpenAIResponsesResponse struct {
|
||||
Status json.RawMessage `json:"status"`
|
||||
Error any `json:"error,omitempty"`
|
||||
IncompleteDetails *IncompleteDetails `json:"incomplete_details,omitempty"`
|
||||
Instructions string `json:"instructions"`
|
||||
Instructions json.RawMessage `json:"instructions"`
|
||||
MaxOutputTokens int `json:"max_output_tokens"`
|
||||
Model string `json:"model"`
|
||||
Output []ResponsesOutput `json:"output"`
|
||||
|
||||
@@ -28,6 +28,18 @@ const (
|
||||
MsgBatchTooMany = "common.batch_too_many"
|
||||
)
|
||||
|
||||
// Auth middleware messages
|
||||
const (
|
||||
MsgAuthNotLoggedIn = "auth.not_logged_in"
|
||||
MsgAuthAccessTokenInvalid = "auth.access_token_invalid"
|
||||
MsgAuthUserInfoInvalid = "auth.user_info_invalid"
|
||||
MsgAuthUserIdNotProvided = "auth.user_id_not_provided"
|
||||
MsgAuthUserIdFormatError = "auth.user_id_format_error"
|
||||
MsgAuthUserIdMismatch = "auth.user_id_mismatch"
|
||||
MsgAuthUserBanned = "auth.user_banned"
|
||||
MsgAuthInsufficientPrivilege = "auth.insufficient_privilege"
|
||||
)
|
||||
|
||||
// Token related messages
|
||||
const (
|
||||
MsgTokenNameTooLong = "token.name_too_long"
|
||||
@@ -101,6 +113,7 @@ const (
|
||||
MsgUserTelegramIdEmpty = "user.telegram_id_empty"
|
||||
MsgUserTelegramNotBound = "user.telegram_not_bound"
|
||||
MsgUserLinuxDOIdEmpty = "user.linux_do_id_empty"
|
||||
MsgUserQuotaChangeZero = "user.quota_change_zero"
|
||||
)
|
||||
|
||||
// Quota related messages
|
||||
|
||||
+12
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
# Common messages
|
||||
common.invalid_params: "Invalid parameters"
|
||||
common.database_error: "Database error, please try again later"
|
||||
common.database_error: "Database error, please contact the administrator"
|
||||
common.retry_later: "Please try again later"
|
||||
common.generate_failed: "Generation failed"
|
||||
common.not_found: "Not found"
|
||||
@@ -23,6 +23,16 @@ common.already_exists: "Already exists"
|
||||
common.name_cannot_be_empty: "Name cannot be empty"
|
||||
common.batch_too_many: "Too many items in batch request, maximum is {{.Max}}"
|
||||
|
||||
# Auth middleware messages
|
||||
auth.not_logged_in: "Unauthorized, not logged in and no access token provided"
|
||||
auth.access_token_invalid: "Unauthorized, invalid access token"
|
||||
auth.user_info_invalid: "Unauthorized, invalid user info"
|
||||
auth.user_id_not_provided: "Unauthorized, New-Api-User header not provided"
|
||||
auth.user_id_format_error: "Unauthorized, New-Api-User header format error"
|
||||
auth.user_id_mismatch: "Unauthorized, New-Api-User does not match logged in user"
|
||||
auth.user_banned: "User has been banned"
|
||||
auth.insufficient_privilege: "Unauthorized, insufficient privileges"
|
||||
|
||||
# Token messages
|
||||
token.name_too_long: "Token name is too long"
|
||||
token.quota_negative: "Quota value cannot be negative"
|
||||
@@ -91,6 +101,7 @@ user.wechat_id_empty: "WeChat ID is empty!"
|
||||
user.telegram_id_empty: "Telegram ID is empty!"
|
||||
user.telegram_not_bound: "This Telegram account is not bound"
|
||||
user.linux_do_id_empty: "Linux DO ID is empty!"
|
||||
user.quota_change_zero: "Quota change amount cannot be zero"
|
||||
|
||||
# Quota messages
|
||||
quota.negative: "Quota cannot be negative!"
|
||||
|
||||
+12
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
# Common messages
|
||||
common.invalid_params: "无效的参数"
|
||||
common.database_error: "数据库错误,请稍后重试"
|
||||
common.database_error: "数据库出错,请联系管理员"
|
||||
common.retry_later: "请稍后重试"
|
||||
common.generate_failed: "生成失败"
|
||||
common.not_found: "未找到"
|
||||
@@ -24,6 +24,16 @@ common.already_exists: "已存在"
|
||||
common.name_cannot_be_empty: "名称不能为空"
|
||||
common.batch_too_many: "批量请求数量过多,最多 {{.Max}} 条"
|
||||
|
||||
# Auth middleware messages
|
||||
auth.not_logged_in: "无权进行此操作,未登录且未提供 access token"
|
||||
auth.access_token_invalid: "无权进行此操作,access token 无效"
|
||||
auth.user_info_invalid: "无权进行此操作,用户信息无效"
|
||||
auth.user_id_not_provided: "无权进行此操作,未提供 New-Api-User"
|
||||
auth.user_id_format_error: "无权进行此操作,New-Api-User 格式错误"
|
||||
auth.user_id_mismatch: "无权进行此操作,New-Api-User 与登录用户不匹配"
|
||||
auth.user_banned: "用户已被封禁"
|
||||
auth.insufficient_privilege: "无权进行此操作,权限不足"
|
||||
|
||||
# Token messages
|
||||
token.name_too_long: "令牌名称过长"
|
||||
token.quota_negative: "额度值不能为负数"
|
||||
@@ -92,6 +102,7 @@ user.wechat_id_empty: "WeChat id 为空!"
|
||||
user.telegram_id_empty: "Telegram id 为空!"
|
||||
user.telegram_not_bound: "该 Telegram 账户未绑定"
|
||||
user.linux_do_id_empty: "Linux DO id 为空!"
|
||||
user.quota_change_zero: "额度变更量不能为0"
|
||||
|
||||
# Quota messages
|
||||
quota.negative: "额度不能为负数!"
|
||||
|
||||
+12
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
# Common messages
|
||||
common.invalid_params: "無效的參數"
|
||||
common.database_error: "資料庫錯誤,請稍後重試"
|
||||
common.database_error: "資料庫出錯,請聯繫管理員"
|
||||
common.retry_later: "請稍後重試"
|
||||
common.generate_failed: "生成失敗"
|
||||
common.not_found: "未找到"
|
||||
@@ -24,6 +24,16 @@ common.already_exists: "已存在"
|
||||
common.name_cannot_be_empty: "名稱不能為空"
|
||||
common.batch_too_many: "批次請求數量過多,最多 {{.Max}} 條"
|
||||
|
||||
# Auth middleware messages
|
||||
auth.not_logged_in: "無權進行此操作,未登入且未提供 access token"
|
||||
auth.access_token_invalid: "無權進行此操作,access token 無效"
|
||||
auth.user_info_invalid: "無權進行此操作,使用者資訊無效"
|
||||
auth.user_id_not_provided: "無權進行此操作,未提供 New-Api-User"
|
||||
auth.user_id_format_error: "無權進行此操作,New-Api-User 格式錯誤"
|
||||
auth.user_id_mismatch: "無權進行此操作,New-Api-User 與登入使用者不匹配"
|
||||
auth.user_banned: "使用者已被封禁"
|
||||
auth.insufficient_privilege: "無權進行此操作,權限不足"
|
||||
|
||||
# Token messages
|
||||
token.name_too_long: "令牌名稱過長"
|
||||
token.quota_negative: "額度值不能為負數"
|
||||
@@ -92,6 +102,7 @@ user.wechat_id_empty: "WeChat id 為空!"
|
||||
user.telegram_id_empty: "Telegram id 為空!"
|
||||
user.telegram_not_bound: "該 Telegram 帳號未綁定"
|
||||
user.linux_do_id_empty: "Linux DO id 為空!"
|
||||
user.quota_change_zero: "額度變更量不能為0"
|
||||
|
||||
# Quota messages
|
||||
quota.negative: "額度不能為負數!"
|
||||
|
||||
+57
-20
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/i18n"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
@@ -17,6 +19,7 @@ import (
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func validUserInfo(username string, role int) bool {
|
||||
@@ -43,17 +46,33 @@ func authHelper(c *gin.Context, minRole int) {
|
||||
if accessToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "无权进行此操作,未登录且未提供 access token",
|
||||
"message": common.TranslateMessage(c, i18n.MsgAuthNotLoggedIn),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
user := model.ValidateAccessToken(accessToken)
|
||||
user, authErr := model.ValidateAccessToken(accessToken)
|
||||
if authErr != nil {
|
||||
if errors.Is(authErr, model.ErrDatabase) {
|
||||
common.SysLog("ValidateAccessToken database error: " + authErr.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": common.TranslateMessage(c, i18n.MsgDatabaseError),
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": common.TranslateMessage(c, i18n.MsgAuthAccessTokenInvalid),
|
||||
})
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if user != nil && user.Username != "" {
|
||||
if !validUserInfo(user.Username, user.Role) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无权进行此操作,用户信息无效",
|
||||
"message": common.TranslateMessage(c, i18n.MsgAuthUserInfoInvalid),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -67,7 +86,7 @@ func authHelper(c *gin.Context, minRole int) {
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无权进行此操作,access token 无效",
|
||||
"message": common.TranslateMessage(c, i18n.MsgAuthAccessTokenInvalid),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -78,7 +97,7 @@ func authHelper(c *gin.Context, minRole int) {
|
||||
if apiUserIdStr == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "无权进行此操作,未提供 New-Api-User",
|
||||
"message": common.TranslateMessage(c, i18n.MsgAuthUserIdNotProvided),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -87,7 +106,7 @@ func authHelper(c *gin.Context, minRole int) {
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "无权进行此操作,New-Api-User 格式错误",
|
||||
"message": common.TranslateMessage(c, i18n.MsgAuthUserIdFormatError),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -96,7 +115,7 @@ func authHelper(c *gin.Context, minRole int) {
|
||||
if id != apiUserId {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "无权进行此操作,New-Api-User 与登录用户不匹配",
|
||||
"message": common.TranslateMessage(c, i18n.MsgAuthUserIdMismatch),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -104,7 +123,7 @@ func authHelper(c *gin.Context, minRole int) {
|
||||
if status.(int) == common.UserStatusDisabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "用户已被封禁",
|
||||
"message": common.TranslateMessage(c, i18n.MsgAuthUserBanned),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -112,7 +131,7 @@ func authHelper(c *gin.Context, minRole int) {
|
||||
if role.(int) < minRole {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无权进行此操作,权限不足",
|
||||
"message": common.TranslateMessage(c, i18n.MsgAuthInsufficientPrivilege),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -120,7 +139,7 @@ func authHelper(c *gin.Context, minRole int) {
|
||||
if !validUserInfo(username.(string), role.(int)) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无权进行此操作,用户信息无效",
|
||||
"message": common.TranslateMessage(c, i18n.MsgAuthUserInfoInvalid),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -198,7 +217,7 @@ func TokenAuthReadOnly() func(c *gin.Context) {
|
||||
if key == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "未提供 Authorization 请求头",
|
||||
"message": common.TranslateMessage(c, i18n.MsgTokenNotProvided),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -212,19 +231,28 @@ func TokenAuthReadOnly() func(c *gin.Context) {
|
||||
|
||||
token, err := model.GetTokenByKey(key, false)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "无效的令牌",
|
||||
})
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": common.TranslateMessage(c, i18n.MsgTokenInvalid),
|
||||
})
|
||||
} else {
|
||||
common.SysLog("TokenAuthReadOnly GetTokenByKey database error: " + err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": common.TranslateMessage(c, i18n.MsgDatabaseError),
|
||||
})
|
||||
}
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
userCache, err := model.GetUserCache(token.UserId)
|
||||
if err != nil {
|
||||
common.SysLog(fmt.Sprintf("TokenAuthReadOnly GetUserCache error for user %d: %v", token.UserId, err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
"message": common.TranslateMessage(c, i18n.MsgDatabaseError),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -232,7 +260,7 @@ func TokenAuthReadOnly() func(c *gin.Context) {
|
||||
if userCache.Status != common.UserStatusEnabled {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": "用户已被封禁",
|
||||
"message": common.TranslateMessage(c, i18n.MsgAuthUserBanned),
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -309,7 +337,14 @@ func TokenAuth() func(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
abortWithOpenAiMessage(c, http.StatusUnauthorized, err.Error())
|
||||
if errors.Is(err, model.ErrDatabase) {
|
||||
common.SysLog("TokenAuth ValidateUserToken database error: " + err.Error())
|
||||
abortWithOpenAiMessage(c, http.StatusInternalServerError,
|
||||
common.TranslateMessage(c, i18n.MsgDatabaseError))
|
||||
} else {
|
||||
abortWithOpenAiMessage(c, http.StatusUnauthorized,
|
||||
common.TranslateMessage(c, i18n.MsgTokenInvalid))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -331,12 +366,14 @@ func TokenAuth() func(c *gin.Context) {
|
||||
|
||||
userCache, err := model.GetUserCache(token.UserId)
|
||||
if err != nil {
|
||||
abortWithOpenAiMessage(c, http.StatusInternalServerError, err.Error())
|
||||
common.SysLog(fmt.Sprintf("TokenAuth GetUserCache error for user %d: %v", token.UserId, err))
|
||||
abortWithOpenAiMessage(c, http.StatusInternalServerError,
|
||||
common.TranslateMessage(c, i18n.MsgDatabaseError))
|
||||
return
|
||||
}
|
||||
userEnabled := userCache.Status == common.UserStatusEnabled
|
||||
if !userEnabled {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, "用户已被封禁")
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, common.TranslateMessage(c, i18n.MsgAuthUserBanned))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import "errors"
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrDatabase = errors.New("database error")
|
||||
)
|
||||
|
||||
// User auth errors
|
||||
var (
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
ErrUserEmptyCredentials = errors.New("empty credentials")
|
||||
)
|
||||
|
||||
// Token auth errors
|
||||
var (
|
||||
ErrTokenNotProvided = errors.New("token not provided")
|
||||
ErrTokenInvalid = errors.New("token invalid")
|
||||
)
|
||||
|
||||
// Redemption errors
|
||||
var ErrRedeemFailed = errors.New("redeem.failed")
|
||||
|
||||
// 2FA errors
|
||||
var ErrTwoFANotEnabled = errors.New("2fa not enabled")
|
||||
+4
-1
@@ -62,6 +62,7 @@ func InitOptionMap() {
|
||||
common.OptionMap["SMTPAccount"] = ""
|
||||
common.OptionMap["SMTPToken"] = ""
|
||||
common.OptionMap["SMTPSSLEnabled"] = strconv.FormatBool(common.SMTPSSLEnabled)
|
||||
common.OptionMap["SMTPForceAuthLogin"] = strconv.FormatBool(common.SMTPForceAuthLogin)
|
||||
common.OptionMap["Notice"] = ""
|
||||
common.OptionMap["About"] = ""
|
||||
common.OptionMap["HomePageContent"] = ""
|
||||
@@ -233,7 +234,7 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
common.ImageDownloadPermission = intValue
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(key, "Enabled") || key == "DefaultCollapseSidebar" || key == "DefaultUseAutoGroup" {
|
||||
if strings.HasSuffix(key, "Enabled") || key == "DefaultCollapseSidebar" || key == "DefaultUseAutoGroup" || key == "SMTPForceAuthLogin" {
|
||||
boolValue := value == "true"
|
||||
switch key {
|
||||
case "PasswordRegisterEnabled":
|
||||
@@ -308,6 +309,8 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
setting.StopOnSensitiveEnabled = boolValue
|
||||
case "SMTPSSLEnabled":
|
||||
common.SMTPSSLEnabled = boolValue
|
||||
case "SMTPForceAuthLogin":
|
||||
common.SMTPForceAuthLogin = boolValue
|
||||
case "WorkerAllowHttpImageRequestEnabled":
|
||||
system_setting.WorkerAllowHttpImageRequestEnabled = boolValue
|
||||
case "DefaultUseAutoGroup":
|
||||
|
||||
@@ -11,9 +11,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ErrRedeemFailed is returned when redemption fails due to database error
|
||||
var ErrRedeemFailed = errors.New("redeem.failed")
|
||||
|
||||
type Redemption struct {
|
||||
Id int `json:"id"`
|
||||
UserId int `json:"user_id"`
|
||||
|
||||
+9
-18
@@ -187,19 +187,14 @@ func SearchUserTokens(userId int, keyword string, token string, offset int, limi
|
||||
|
||||
func ValidateUserToken(key string) (token *Token, err error) {
|
||||
if key == "" {
|
||||
return nil, errors.New("未提供令牌")
|
||||
return nil, ErrTokenNotProvided
|
||||
}
|
||||
token, err = GetTokenByKey(key, false)
|
||||
if err == nil {
|
||||
if token.Status == common.TokenStatusExhausted {
|
||||
keyPrefix := key[:3]
|
||||
keySuffix := key[len(key)-3:]
|
||||
return token, errors.New("该令牌额度已用尽 TokenStatusExhausted[sk-" + keyPrefix + "***" + keySuffix + "]")
|
||||
} else if token.Status == common.TokenStatusExpired {
|
||||
return token, errors.New("该令牌已过期")
|
||||
}
|
||||
if token.Status != common.TokenStatusEnabled {
|
||||
return token, errors.New("该令牌状态不可用")
|
||||
if token.Status == common.TokenStatusExhausted ||
|
||||
token.Status == common.TokenStatusExpired ||
|
||||
token.Status != common.TokenStatusEnabled {
|
||||
return token, ErrTokenInvalid
|
||||
}
|
||||
if token.ExpiredTime != -1 && token.ExpiredTime < common.GetTimestamp() {
|
||||
if !common.RedisEnabled {
|
||||
@@ -209,29 +204,25 @@ func ValidateUserToken(key string) (token *Token, err error) {
|
||||
common.SysLog("failed to update token status" + err.Error())
|
||||
}
|
||||
}
|
||||
return token, errors.New("该令牌已过期")
|
||||
return token, ErrTokenInvalid
|
||||
}
|
||||
if !token.UnlimitedQuota && token.RemainQuota <= 0 {
|
||||
if !common.RedisEnabled {
|
||||
// in this case, we can make sure the token is exhausted
|
||||
token.Status = common.TokenStatusExhausted
|
||||
err := token.SelectUpdate()
|
||||
if err != nil {
|
||||
common.SysLog("failed to update token status" + err.Error())
|
||||
}
|
||||
}
|
||||
keyPrefix := key[:3]
|
||||
keySuffix := key[len(key)-3:]
|
||||
return token, fmt.Errorf("[sk-%s***%s] 该令牌额度已用尽 !token.UnlimitedQuota && token.RemainQuota = %d", keyPrefix, keySuffix, token.RemainQuota)
|
||||
return token, ErrTokenInvalid
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
common.SysLog("ValidateUserToken: failed to get token: " + err.Error())
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("无效的令牌")
|
||||
} else {
|
||||
return nil, errors.New("无效的令牌,数据库查询出错,请联系管理员")
|
||||
return nil, ErrTokenInvalid
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %v", ErrDatabase, err)
|
||||
}
|
||||
|
||||
func GetTokenByIds(id int, userId int) (*Token, error) {
|
||||
|
||||
+23
-9
@@ -12,17 +12,19 @@ import (
|
||||
)
|
||||
|
||||
type TopUp struct {
|
||||
Id int `json:"id"`
|
||||
UserId int `json:"user_id" gorm:"index"`
|
||||
Amount int64 `json:"amount"`
|
||||
Money float64 `json:"money"`
|
||||
TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"`
|
||||
PaymentMethod string `json:"payment_method" gorm:"type:varchar(50)"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
CompleteTime int64 `json:"complete_time"`
|
||||
Status string `json:"status"`
|
||||
Id int `json:"id"`
|
||||
UserId int `json:"user_id" gorm:"index"`
|
||||
Amount int64 `json:"amount"`
|
||||
Money float64 `json:"money"`
|
||||
TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"`
|
||||
PaymentMethod string `json:"payment_method" gorm:"type:varchar(50)"`
|
||||
CreateTime int64 `json:"create_time"`
|
||||
CompleteTime int64 `json:"complete_time"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
var ErrPaymentMethodMismatch = errors.New("payment method mismatch")
|
||||
|
||||
func (topUp *TopUp) Insert() error {
|
||||
var err error
|
||||
err = DB.Create(topUp).Error
|
||||
@@ -74,6 +76,10 @@ func Recharge(referenceId string, customerId string) (err error) {
|
||||
return errors.New("充值订单不存在")
|
||||
}
|
||||
|
||||
if topUp.PaymentMethod != "stripe" {
|
||||
return ErrPaymentMethodMismatch
|
||||
}
|
||||
|
||||
if topUp.Status != common.TopUpStatusPending {
|
||||
return errors.New("充值订单状态错误")
|
||||
}
|
||||
@@ -325,6 +331,10 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
|
||||
return errors.New("充值订单不存在")
|
||||
}
|
||||
|
||||
if topUp.PaymentMethod != "creem" {
|
||||
return ErrPaymentMethodMismatch
|
||||
}
|
||||
|
||||
if topUp.Status != common.TopUpStatusPending {
|
||||
return errors.New("充值订单状态错误")
|
||||
}
|
||||
@@ -396,6 +406,10 @@ func RechargeWaffo(tradeNo string) (err error) {
|
||||
return errors.New("充值订单不存在")
|
||||
}
|
||||
|
||||
if topUp.PaymentMethod != "waffo" {
|
||||
return ErrPaymentMethodMismatch
|
||||
}
|
||||
|
||||
if topUp.Status == common.TopUpStatusSuccess {
|
||||
return nil // 幂等:已成功直接返回
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var ErrTwoFANotEnabled = errors.New("用户未启用2FA")
|
||||
|
||||
// TwoFA 用户2FA设置表
|
||||
type TwoFA struct {
|
||||
Id int `json:"id" gorm:"primaryKey"`
|
||||
|
||||
+23
-14
@@ -523,7 +523,6 @@ func (user *User) Edit(updatePassword bool) error {
|
||||
"username": newUser.Username,
|
||||
"display_name": newUser.DisplayName,
|
||||
"group": newUser.Group,
|
||||
"quota": newUser.Quota,
|
||||
"remark": newUser.Remark,
|
||||
}
|
||||
if updatePassword {
|
||||
@@ -598,13 +597,19 @@ func (user *User) ValidateAndFill() (err error) {
|
||||
password := user.Password
|
||||
username := strings.TrimSpace(user.Username)
|
||||
if username == "" || password == "" {
|
||||
return errors.New("用户名或密码为空")
|
||||
return ErrUserEmptyCredentials
|
||||
}
|
||||
// find by username or email
|
||||
err = DB.Where("username = ? OR email = ?", username, username).First(user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
return fmt.Errorf("%w: %v", ErrDatabase, err)
|
||||
}
|
||||
// find buy username or email
|
||||
DB.Where("username = ? OR email = ?", username, username).First(user)
|
||||
okay := common.ValidatePasswordAndHash(password, user.Password)
|
||||
if !okay || user.Status != common.UserStatusEnabled {
|
||||
return errors.New("用户名或密码错误,或用户已被封禁")
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -755,16 +760,20 @@ func IsAdmin(userId int) bool {
|
||||
// return user.Status == common.UserStatusEnabled, nil
|
||||
//}
|
||||
|
||||
func ValidateAccessToken(token string) (user *User) {
|
||||
func ValidateAccessToken(token string) (*User, error) {
|
||||
if token == "" {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
token = strings.Replace(token, "Bearer ", "", 1)
|
||||
user = &User{}
|
||||
if DB.Where("access_token = ?", token).First(user).RowsAffected == 1 {
|
||||
return user
|
||||
user := &User{}
|
||||
err := DB.Where("access_token = ?", token).First(user).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %v", ErrDatabase, err)
|
||||
}
|
||||
return nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetUserQuota gets quota from Redis first, falls back to DB if needed
|
||||
@@ -896,7 +905,7 @@ func increaseUserQuota(id int, quota int) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func DecreaseUserQuota(id int, quota int) (err error) {
|
||||
func DecreaseUserQuota(id int, quota int, db bool) (err error) {
|
||||
if quota < 0 {
|
||||
return errors.New("quota 不能为负数!")
|
||||
}
|
||||
@@ -906,7 +915,7 @@ func DecreaseUserQuota(id int, quota int) (err error) {
|
||||
common.SysLog("failed to decrease user quota: " + err.Error())
|
||||
}
|
||||
})
|
||||
if common.BatchUpdateEnabled {
|
||||
if !db && common.BatchUpdateEnabled {
|
||||
addNewRecord(BatchUpdateTypeUserQuota, id, -quota)
|
||||
return nil
|
||||
}
|
||||
@@ -928,7 +937,7 @@ func DeltaUpdateUserQuota(id int, delta int) (err error) {
|
||||
if delta > 0 {
|
||||
return IncreaseUserQuota(id, delta, false)
|
||||
} else {
|
||||
return DecreaseUserQuota(id, -delta)
|
||||
return DecreaseUserQuota(id, -delta, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe
|
||||
Type: "adaptive",
|
||||
}
|
||||
claudeRequest.OutputConfig = json.RawMessage(fmt.Sprintf(`{"effort":"%s"}`, effortLevel))
|
||||
claudeRequest.TopP = common.GetPointer[float64](0)
|
||||
claudeRequest.TopP = nil
|
||||
claudeRequest.Temperature = common.GetPointer[float64](1.0)
|
||||
} else if model_setting.GetClaudeSettings().ThinkingAdapterEnabled &&
|
||||
strings.HasSuffix(textRequest.Model, "-thinking") {
|
||||
@@ -258,7 +258,7 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe
|
||||
formatMessages = formatMessages[:len(formatMessages)-1]
|
||||
}
|
||||
}
|
||||
if fmtMessage.Content == nil {
|
||||
if fmtMessage.Content == nil || (fmtMessage.IsStringContent() && fmtMessage.StringContent() == "") {
|
||||
fmtMessage.SetStringContent("...")
|
||||
}
|
||||
formatMessages = append(formatMessages, fmtMessage)
|
||||
@@ -274,14 +274,16 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe
|
||||
if message.Role == "system" {
|
||||
// 根据Claude API规范,system字段使用数组格式更有通用性
|
||||
if message.IsStringContent() {
|
||||
systemMessages = append(systemMessages, dto.ClaudeMediaMessage{
|
||||
Type: "text",
|
||||
Text: common.GetPointer[string](message.StringContent()),
|
||||
})
|
||||
if text := message.StringContent(); text != "" {
|
||||
systemMessages = append(systemMessages, dto.ClaudeMediaMessage{
|
||||
Type: "text",
|
||||
Text: common.GetPointer[string](text),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 支持复合内容的system消息(虽然不常见,但需要考虑完整性)
|
||||
for _, ctx := range message.ParseContent() {
|
||||
if ctx.Type == "text" {
|
||||
if ctx.Type == "text" && ctx.Text != "" {
|
||||
systemMessages = append(systemMessages, dto.ClaudeMediaMessage{
|
||||
Type: "text",
|
||||
Text: common.GetPointer[string](ctx.Text),
|
||||
@@ -339,16 +341,22 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe
|
||||
}
|
||||
}
|
||||
} else if message.IsStringContent() && message.ToolCalls == nil {
|
||||
claudeMessage.Content = message.StringContent()
|
||||
text := message.StringContent()
|
||||
if text == "" {
|
||||
text = "..."
|
||||
}
|
||||
claudeMessage.Content = text
|
||||
} else {
|
||||
claudeMediaMessages := make([]dto.ClaudeMediaMessage, 0)
|
||||
for _, mediaMessage := range message.ParseContent() {
|
||||
switch mediaMessage.Type {
|
||||
case "text":
|
||||
claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{
|
||||
Type: "text",
|
||||
Text: common.GetPointer[string](mediaMessage.Text),
|
||||
})
|
||||
if mediaMessage.Text != "" {
|
||||
claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{
|
||||
Type: "text",
|
||||
Text: common.GetPointer[string](mediaMessage.Text),
|
||||
})
|
||||
}
|
||||
default:
|
||||
source := mediaMessage.ToFileSource()
|
||||
if source == nil {
|
||||
|
||||
@@ -78,7 +78,10 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
|
||||
return request, nil
|
||||
if info.RelayMode != constant.RelayModeImagesGenerations {
|
||||
return nil, fmt.Errorf("unsupported image relay mode: %d", info.RelayMode)
|
||||
}
|
||||
return oaiImage2MiniMaxImageRequest(request), nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
@@ -121,6 +124,9 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
if info.RelayMode == constant.RelayModeAudioSpeech {
|
||||
return handleTTSResponse(c, resp, info)
|
||||
}
|
||||
if info.RelayMode == constant.RelayModeImagesGenerations {
|
||||
return miniMaxImageHandler(c, resp, info)
|
||||
}
|
||||
|
||||
switch info.RelayFormat {
|
||||
case types.RelayFormatClaude:
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package minimax
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
relayconstant "github.com/QuantumNous/new-api/relay/constant"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestGetRequestURLForImageGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
info := &relaycommon.RelayInfo{
|
||||
RelayMode: relayconstant.RelayModeImagesGenerations,
|
||||
ChannelMeta: &relaycommon.ChannelMeta{
|
||||
ChannelBaseUrl: "https://api.minimax.chat",
|
||||
},
|
||||
}
|
||||
|
||||
got, err := GetRequestURL(info)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRequestURL returned error: %v", err)
|
||||
}
|
||||
|
||||
want := "https://api.minimax.chat/v1/image_generation"
|
||||
if got != want {
|
||||
t.Fatalf("GetRequestURL() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertImageRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
adaptor := &Adaptor{}
|
||||
info := &relaycommon.RelayInfo{
|
||||
RelayMode: relayconstant.RelayModeImagesGenerations,
|
||||
OriginModelName: "image-01",
|
||||
}
|
||||
request := dto.ImageRequest{
|
||||
Model: "image-01",
|
||||
Prompt: "a red fox in snowfall",
|
||||
Size: "1536x1024",
|
||||
ResponseFormat: "url",
|
||||
N: uintPtr(2),
|
||||
}
|
||||
|
||||
got, err := adaptor.ConvertImageRequest(gin.CreateTestContextOnly(httptest.NewRecorder(), gin.New()), info, request)
|
||||
if err != nil {
|
||||
t.Fatalf("ConvertImageRequest returned error: %v", err)
|
||||
}
|
||||
|
||||
body, err := json.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal returned error: %v", err)
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
t.Fatalf("json.Unmarshal returned error: %v", err)
|
||||
}
|
||||
|
||||
if payload["model"] != "image-01" {
|
||||
t.Fatalf("model = %#v, want %q", payload["model"], "image-01")
|
||||
}
|
||||
if payload["prompt"] != request.Prompt {
|
||||
t.Fatalf("prompt = %#v, want %q", payload["prompt"], request.Prompt)
|
||||
}
|
||||
if payload["n"] != float64(2) {
|
||||
t.Fatalf("n = %#v, want 2", payload["n"])
|
||||
}
|
||||
if payload["aspect_ratio"] != "3:2" {
|
||||
t.Fatalf("aspect_ratio = %#v, want %q", payload["aspect_ratio"], "3:2")
|
||||
}
|
||||
if payload["response_format"] != "url" {
|
||||
t.Fatalf("response_format = %#v, want %q", payload["response_format"], "url")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoResponseForImageGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
recorder := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(recorder)
|
||||
|
||||
info := &relaycommon.RelayInfo{
|
||||
RelayMode: relayconstant.RelayModeImagesGenerations,
|
||||
StartTime: time.Unix(1700000000, 0),
|
||||
}
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: httptest.NewRecorder().Result().Body,
|
||||
}
|
||||
resp.Body = ioNopCloser(`{"data":{"image_urls":["https://example.com/minimax.png"]}}`)
|
||||
|
||||
adaptor := &Adaptor{}
|
||||
usage, err := adaptor.DoResponse(c, resp, info)
|
||||
if err != nil {
|
||||
t.Fatalf("DoResponse returned error: %v", err)
|
||||
}
|
||||
if usage == nil {
|
||||
t.Fatalf("DoResponse returned nil usage")
|
||||
}
|
||||
|
||||
body := recorder.Body.String()
|
||||
if !strings.Contains(body, `"url":"https://example.com/minimax.png"`) {
|
||||
t.Fatalf("response body = %s, want OpenAI image response with image URL", body)
|
||||
}
|
||||
if strings.Contains(body, `"image_urls"`) {
|
||||
t.Fatalf("response body = %s, should not expose raw MiniMax image_urls payload", body)
|
||||
}
|
||||
}
|
||||
|
||||
type nopReadCloser struct {
|
||||
*strings.Reader
|
||||
}
|
||||
|
||||
func (n nopReadCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ioNopCloser(body string) nopReadCloser {
|
||||
return nopReadCloser{Reader: strings.NewReader(body)}
|
||||
}
|
||||
|
||||
func uintPtr(v uint) *uint {
|
||||
return &v
|
||||
}
|
||||
@@ -8,6 +8,8 @@ var ModelList = []string{
|
||||
"abab6-chat",
|
||||
"abab5.5-chat",
|
||||
"abab5.5s-chat",
|
||||
"MiniMax-M2.7",
|
||||
"MiniMax-M2.7-highspeed",
|
||||
"speech-2.5-hd-preview",
|
||||
"speech-2.5-turbo-preview",
|
||||
"speech-02-hd",
|
||||
@@ -19,6 +21,8 @@ var ModelList = []string{
|
||||
"MiniMax-M2",
|
||||
"MiniMax-M2.5",
|
||||
"MiniMax-M2.5-highspeed",
|
||||
"image-01",
|
||||
"image-01-live",
|
||||
}
|
||||
|
||||
var ChannelName = "minimax"
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
package minimax
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MiniMaxImageRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
AspectRatio string `json:"aspect_ratio,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
N int `json:"n,omitempty"`
|
||||
PromptOptimizer *bool `json:"prompt_optimizer,omitempty"`
|
||||
AigcWatermark *bool `json:"aigc_watermark,omitempty"`
|
||||
}
|
||||
|
||||
type MiniMaxImageResponse struct {
|
||||
ID string `json:"id"`
|
||||
Data struct {
|
||||
ImageURLs []string `json:"image_urls"`
|
||||
ImageBase64 []string `json:"image_base64"`
|
||||
} `json:"data"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
BaseResp struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
StatusMsg string `json:"status_msg"`
|
||||
} `json:"base_resp"`
|
||||
}
|
||||
|
||||
func oaiImage2MiniMaxImageRequest(request dto.ImageRequest) MiniMaxImageRequest {
|
||||
responseFormat := normalizeMiniMaxResponseFormat(request.ResponseFormat)
|
||||
minimaxRequest := MiniMaxImageRequest{
|
||||
Model: request.Model,
|
||||
Prompt: request.Prompt,
|
||||
ResponseFormat: responseFormat,
|
||||
N: 1,
|
||||
AigcWatermark: request.Watermark,
|
||||
}
|
||||
|
||||
if request.Model == "" {
|
||||
minimaxRequest.Model = "image-01"
|
||||
}
|
||||
if request.N != nil && *request.N > 0 {
|
||||
minimaxRequest.N = int(*request.N)
|
||||
}
|
||||
if aspectRatio := aspectRatioFromImageRequest(request); aspectRatio != "" {
|
||||
minimaxRequest.AspectRatio = aspectRatio
|
||||
}
|
||||
if raw, ok := request.Extra["prompt_optimizer"]; ok {
|
||||
var promptOptimizer bool
|
||||
if err := common.Unmarshal(raw, &promptOptimizer); err == nil {
|
||||
minimaxRequest.PromptOptimizer = &promptOptimizer
|
||||
}
|
||||
}
|
||||
|
||||
return minimaxRequest
|
||||
}
|
||||
|
||||
func aspectRatioFromImageRequest(request dto.ImageRequest) string {
|
||||
if raw, ok := request.Extra["aspect_ratio"]; ok {
|
||||
var aspectRatio string
|
||||
if err := common.Unmarshal(raw, &aspectRatio); err == nil && aspectRatio != "" {
|
||||
return aspectRatio
|
||||
}
|
||||
}
|
||||
|
||||
switch request.Size {
|
||||
case "1024x1024":
|
||||
return "1:1"
|
||||
case "1792x1024":
|
||||
return "16:9"
|
||||
case "1024x1792":
|
||||
return "9:16"
|
||||
case "1536x1024", "1248x832":
|
||||
return "3:2"
|
||||
case "1024x1536", "832x1248":
|
||||
return "2:3"
|
||||
case "1152x864":
|
||||
return "4:3"
|
||||
case "864x1152":
|
||||
return "3:4"
|
||||
case "1344x576":
|
||||
return "21:9"
|
||||
}
|
||||
|
||||
width, height, ok := parseImageSize(request.Size)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
ratio := reduceAspectRatio(width, height)
|
||||
switch ratio {
|
||||
case "1:1", "16:9", "4:3", "3:2", "2:3", "3:4", "9:16", "21:9":
|
||||
return ratio
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func parseImageSize(size string) (int, int, bool) {
|
||||
parts := strings.Split(size, "x")
|
||||
if len(parts) != 2 {
|
||||
return 0, 0, false
|
||||
}
|
||||
width, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
height, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
if width <= 0 || height <= 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return width, height, true
|
||||
}
|
||||
|
||||
func reduceAspectRatio(width, height int) string {
|
||||
divisor := gcd(width, height)
|
||||
return fmt.Sprintf("%d:%d", width/divisor, height/divisor)
|
||||
}
|
||||
|
||||
func gcd(a, b int) int {
|
||||
for b != 0 {
|
||||
a, b = b, a%b
|
||||
}
|
||||
if a == 0 {
|
||||
return 1
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func normalizeMiniMaxResponseFormat(responseFormat string) string {
|
||||
switch strings.ToLower(responseFormat) {
|
||||
case "", "url":
|
||||
return "url"
|
||||
case "b64_json", "base64":
|
||||
return "base64"
|
||||
default:
|
||||
return responseFormat
|
||||
}
|
||||
}
|
||||
|
||||
func responseMiniMax2OpenAIImage(response *MiniMaxImageResponse, info *relaycommon.RelayInfo) (*dto.ImageResponse, error) {
|
||||
imageResponse := &dto.ImageResponse{
|
||||
Created: info.StartTime.Unix(),
|
||||
}
|
||||
|
||||
for _, imageURL := range response.Data.ImageURLs {
|
||||
imageResponse.Data = append(imageResponse.Data, dto.ImageData{Url: imageURL})
|
||||
}
|
||||
for _, imageBase64 := range response.Data.ImageBase64 {
|
||||
imageResponse.Data = append(imageResponse.Data, dto.ImageData{B64Json: imageBase64})
|
||||
}
|
||||
if len(response.Metadata) > 0 {
|
||||
metadata, err := common.Marshal(response.Metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageResponse.Metadata = metadata
|
||||
}
|
||||
|
||||
return imageResponse, nil
|
||||
}
|
||||
|
||||
func miniMaxImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.Usage, *types.NewAPIError) {
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError)
|
||||
}
|
||||
service.CloseResponseBodyGracefully(resp)
|
||||
|
||||
var minimaxResponse MiniMaxImageResponse
|
||||
if err := common.Unmarshal(responseBody, &minimaxResponse); err != nil {
|
||||
return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
|
||||
}
|
||||
if minimaxResponse.BaseResp.StatusCode != 0 {
|
||||
return nil, types.WithOpenAIError(types.OpenAIError{
|
||||
Message: minimaxResponse.BaseResp.StatusMsg,
|
||||
Type: "minimax_image_error",
|
||||
Code: fmt.Sprintf("%d", minimaxResponse.BaseResp.StatusCode),
|
||||
}, resp.StatusCode)
|
||||
}
|
||||
|
||||
openAIResponse, err := responseMiniMax2OpenAIImage(&minimaxResponse, info)
|
||||
if err != nil {
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
jsonResponse, err := common.Marshal(openAIResponse)
|
||||
if err != nil {
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
|
||||
c.Writer.Header().Set("Content-Type", "application/json")
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
if _, err := c.Writer.Write(jsonResponse); err != nil {
|
||||
return nil, types.NewError(err, types.ErrorCodeBadResponseBody)
|
||||
}
|
||||
|
||||
return &dto.Usage{}, nil
|
||||
}
|
||||
@@ -21,6 +21,8 @@ func GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
switch info.RelayMode {
|
||||
case constant.RelayModeChatCompletions:
|
||||
return fmt.Sprintf("%s/v1/text/chatcompletion_v2", baseUrl), nil
|
||||
case constant.RelayModeImagesGenerations:
|
||||
return fmt.Sprintf("%s/v1/image_generation", baseUrl), nil
|
||||
case constant.RelayModeAudioSpeech:
|
||||
return fmt.Sprintf("%s/v1/t2a_v2", baseUrl), nil
|
||||
default:
|
||||
|
||||
@@ -136,8 +136,8 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
task = "chat/completions" + task
|
||||
}
|
||||
|
||||
// 特殊处理 responses API
|
||||
if info.RelayMode == relayconstant.RelayModeResponses {
|
||||
// 特殊处理 responses API(包含 compact)
|
||||
if info.RelayMode == relayconstant.RelayModeResponses || info.RelayMode == relayconstant.RelayModeResponsesCompact {
|
||||
responsesApiVersion := "preview"
|
||||
|
||||
subUrl := "/openai/v1/responses"
|
||||
@@ -150,6 +150,11 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
responsesApiVersion = info.ChannelOtherSettings.AzureResponsesVersion
|
||||
}
|
||||
|
||||
// compact 模式追加 /compact
|
||||
if info.RelayMode == relayconstant.RelayModeResponsesCompact {
|
||||
subUrl = subUrl + "/compact"
|
||||
}
|
||||
|
||||
requestURL = fmt.Sprintf("%s?api-version=%s", subUrl, responsesApiVersion)
|
||||
return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, requestURL, info.ChannelType), nil
|
||||
}
|
||||
@@ -369,7 +374,7 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
|
||||
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
|
||||
a.ResponseFormat = request.ResponseFormat
|
||||
if info.RelayMode == relayconstant.RelayModeAudioSpeech {
|
||||
jsonData, err := json.Marshal(request)
|
||||
jsonData, err := common.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshalling object: %w", err)
|
||||
}
|
||||
|
||||
@@ -80,9 +80,9 @@ type AliVideoOutput struct {
|
||||
|
||||
// AliUsage 使用统计
|
||||
type AliUsage struct {
|
||||
Duration int `json:"duration,omitempty"`
|
||||
VideoCount int `json:"video_count,omitempty"`
|
||||
SR int `json:"SR,omitempty"`
|
||||
Duration dto.IntValue `json:"duration,omitempty"`
|
||||
VideoCount dto.IntValue `json:"video_count,omitempty"`
|
||||
SR dto.IntValue `json:"SR,omitempty"`
|
||||
}
|
||||
|
||||
type AliMetadata struct {
|
||||
|
||||
@@ -64,6 +64,9 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
}
|
||||
return fmt.Sprintf("%s/api/paas/v4/embeddings", baseURL), nil
|
||||
case relayconstant.RelayModeImagesGenerations:
|
||||
if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
|
||||
return fmt.Sprintf("%s/images/generations", specialPlan.OpenAIBaseURL), nil
|
||||
}
|
||||
return fmt.Sprintf("%s/api/paas/v4/images/generations", baseURL), nil
|
||||
default:
|
||||
if hasSpecialPlan && specialPlan.OpenAIBaseURL != "" {
|
||||
|
||||
@@ -32,6 +32,7 @@ var paramOverrideKeyAuditPaths = map[string]struct{}{
|
||||
"upstream_model": {},
|
||||
"service_tier": {},
|
||||
"inference_geo": {},
|
||||
"speed": {},
|
||||
}
|
||||
|
||||
type paramOverrideAuditRecorder struct {
|
||||
|
||||
@@ -2038,6 +2038,8 @@ func TestRemoveDisabledFieldsDefaultFiltering(t *testing.T) {
|
||||
input := `{
|
||||
"service_tier":"flex",
|
||||
"inference_geo":"eu",
|
||||
"speed":"fast",
|
||||
"cache_control":{"type":"ephemeral"},
|
||||
"safety_identifier":"user-123",
|
||||
"store":true,
|
||||
"stream_options":{"include_obfuscation":false}
|
||||
@@ -2048,7 +2050,7 @@ func TestRemoveDisabledFieldsDefaultFiltering(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("RemoveDisabledFields returned error: %v", err)
|
||||
}
|
||||
assertJSONEqual(t, `{"store":true}`, string(out))
|
||||
assertJSONEqual(t, `{"cache_control":{"type":"ephemeral"},"store":true}`, string(out))
|
||||
}
|
||||
|
||||
func TestRemoveDisabledFieldsAllowInferenceGeo(t *testing.T) {
|
||||
@@ -2067,6 +2069,22 @@ func TestRemoveDisabledFieldsAllowInferenceGeo(t *testing.T) {
|
||||
assertJSONEqual(t, `{"inference_geo":"eu","store":true}`, string(out))
|
||||
}
|
||||
|
||||
func TestRemoveDisabledFieldsAllowSpeed(t *testing.T) {
|
||||
input := `{
|
||||
"speed":"fast",
|
||||
"store":true
|
||||
}`
|
||||
settings := dto.ChannelOtherSettings{
|
||||
AllowSpeed: true,
|
||||
}
|
||||
|
||||
out, err := RemoveDisabledFields([]byte(input), settings, false)
|
||||
if err != nil {
|
||||
t.Fatalf("RemoveDisabledFields returned error: %v", err)
|
||||
}
|
||||
assertJSONEqual(t, `{"speed":"fast","store":true}`, string(out))
|
||||
}
|
||||
|
||||
func TestApplyParamOverrideWithRelayInfoRecordsOperationAuditInDebugMode(t *testing.T) {
|
||||
originalDebugEnabled := common2.DebugEnabled
|
||||
common2.DebugEnabled = true
|
||||
|
||||
@@ -438,6 +438,7 @@ func genBaseRelayInfo(c *gin.Context, request dto.Request) *RelayInfo {
|
||||
if request != nil {
|
||||
isStream = request.IsStream(c)
|
||||
}
|
||||
c.Set(string(constant.ContextKeyIsStream), isStream)
|
||||
|
||||
// firstResponseTime = time.Now() - 1 second
|
||||
|
||||
@@ -770,6 +771,7 @@ func FailTaskInfo(reason string) *TaskInfo {
|
||||
// RemoveDisabledFields 从请求 JSON 数据中移除渠道设置中禁用的字段
|
||||
// service_tier: 服务层级字段,可能导致额外计费(OpenAI、Claude、Responses API 支持)
|
||||
// inference_geo: Claude 数据驻留推理区域字段(仅 Claude 支持,默认过滤)
|
||||
// speed: Claude 推理速度模式字段(仅 Claude 支持,默认过滤)
|
||||
// store: 数据存储授权字段,涉及用户隐私(仅 OpenAI、Responses API 支持,默认允许透传,禁用后可能导致 Codex 无法使用)
|
||||
// safety_identifier: 安全标识符,用于向 OpenAI 报告违规用户(仅 OpenAI 支持,涉及用户隐私)
|
||||
// stream_options.include_obfuscation: 响应流混淆控制字段(仅 OpenAI Responses API 支持)
|
||||
@@ -798,6 +800,13 @@ func RemoveDisabledFields(jsonData []byte, channelOtherSettings dto.ChannelOther
|
||||
}
|
||||
}
|
||||
|
||||
// 默认移除 speed,除非明确允许(避免意外切换 Claude 推理速度模式)
|
||||
if !channelOtherSettings.AllowSpeed {
|
||||
if _, exists := data["speed"]; exists {
|
||||
delete(data, "speed")
|
||||
}
|
||||
}
|
||||
|
||||
// 默认允许 store 透传,除非明确禁用(禁用可能影响 Codex 使用)
|
||||
if channelOtherSettings.DisableStore {
|
||||
if _, exists := data["store"]; exists {
|
||||
|
||||
+18
-2
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
@@ -13,6 +14,21 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func modelPriceNotConfiguredError(modelName string, userId int) error {
|
||||
if model.IsAdmin(userId) {
|
||||
return fmt.Errorf(
|
||||
"模型 %s 的价格未配置。请前往「系统设置 → 运营设置」开启自用模式,或在「系统设置 → 分组与模型定价设置」中为该模型配置价格;"+
|
||||
"Model %s price not configured. Go to System Settings → Operation Settings to enable self-use mode, or configure the model price in System Settings → Group & Model Pricing.",
|
||||
modelName, modelName,
|
||||
)
|
||||
}
|
||||
return fmt.Errorf(
|
||||
"模型 %s 的价格尚未由管理员配置,暂时无法使用,请联系站点管理员开启该模型;"+
|
||||
"Model %s has not been priced by the administrator yet. Please contact the site administrator to enable this model.",
|
||||
modelName, modelName,
|
||||
)
|
||||
}
|
||||
|
||||
// https://docs.claude.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration
|
||||
const claudeCacheCreation1hMultiplier = 6 / 3.75
|
||||
|
||||
@@ -75,7 +91,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
||||
acceptUnsetRatio = true
|
||||
}
|
||||
if !acceptUnsetRatio {
|
||||
return types.PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请联系管理员设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", matchName, matchName)
|
||||
return types.PriceData{}, modelPriceNotConfiguredError(matchName, info.UserId)
|
||||
}
|
||||
}
|
||||
completionRatio = ratio_setting.GetCompletionRatio(info.OriginModelName)
|
||||
@@ -161,7 +177,7 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types
|
||||
acceptUnsetRatio = true
|
||||
}
|
||||
if !ratioSuccess && !acceptUnsetRatio {
|
||||
return types.PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请联系管理员设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", matchName, matchName)
|
||||
return types.PriceData{}, modelPriceNotConfiguredError(matchName, info.UserId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func ResponsesHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
|
||||
if err != nil {
|
||||
info.OriginModelName = originModelName
|
||||
info.PriceData = originPriceData
|
||||
return types.NewError(err, types.ErrorCodeModelPriceError, types.ErrOptionWithSkipRetry())
|
||||
return types.NewError(err, types.ErrorCodeModelPriceError, types.ErrOptionWithSkipRetry(), types.ErrOptionWithStatusCode(http.StatusBadRequest))
|
||||
}
|
||||
service.PostTextConsumeQuota(c, info, usageDto, nil)
|
||||
|
||||
|
||||
+6
-8
@@ -2,11 +2,9 @@ package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
@@ -63,12 +61,12 @@ func ShouldDisableChannel(channelType int, err *types.NewAPIError) bool {
|
||||
//if err.StatusCode == http.StatusUnauthorized {
|
||||
// return true
|
||||
//}
|
||||
if err.StatusCode == http.StatusForbidden {
|
||||
switch channelType {
|
||||
case constant.ChannelTypeGemini:
|
||||
return true
|
||||
}
|
||||
}
|
||||
//if err.StatusCode == http.StatusForbidden {
|
||||
// switch channelType {
|
||||
// case constant.ChannelTypeGemini:
|
||||
// return true
|
||||
// }
|
||||
//}
|
||||
oaiErr := err.ToOpenAIError()
|
||||
switch oaiErr.Code {
|
||||
case "invalid_api_key":
|
||||
|
||||
@@ -37,7 +37,7 @@ func (w *WalletFunding) PreConsume(amount int) error {
|
||||
if amount <= 0 {
|
||||
return nil
|
||||
}
|
||||
if err := model.DecreaseUserQuota(w.userId, amount); err != nil {
|
||||
if err := model.DecreaseUserQuota(w.userId, amount, false); err != nil {
|
||||
return err
|
||||
}
|
||||
w.consumed = amount
|
||||
@@ -49,7 +49,7 @@ func (w *WalletFunding) Settle(delta int) error {
|
||||
return nil
|
||||
}
|
||||
if delta > 0 {
|
||||
return model.DecreaseUserQuota(w.userId, delta)
|
||||
return model.DecreaseUserQuota(w.userId, delta, false)
|
||||
}
|
||||
return model.IncreaseUserQuota(w.userId, -delta, false)
|
||||
}
|
||||
|
||||
+1
-1
@@ -381,7 +381,7 @@ func PostConsumeQuota(relayInfo *relaycommon.RelayInfo, quota int, preConsumedQu
|
||||
} else {
|
||||
// Wallet
|
||||
if quota > 0 {
|
||||
err = model.DecreaseUserQuota(relayInfo.UserId, quota)
|
||||
err = model.DecreaseUserQuota(relayInfo.UserId, quota, false)
|
||||
} else {
|
||||
err = model.IncreaseUserQuota(relayInfo.UserId, -quota, false)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func taskAdjustFunding(task *model.Task, delta int) error {
|
||||
return model.PostConsumeUserSubscriptionDelta(task.PrivateData.SubscriptionId, int64(delta))
|
||||
}
|
||||
if delta > 0 {
|
||||
return model.DecreaseUserQuota(task.UserId, delta)
|
||||
return model.DecreaseUserQuota(task.UserId, delta, false)
|
||||
}
|
||||
return model.IncreaseUserQuota(task.UserId, -delta, false)
|
||||
}
|
||||
|
||||
@@ -361,6 +361,10 @@ func UpdateModelPriceByJSONString(jsonStr string) error {
|
||||
func GetModelPrice(name string, printErr bool) (float64, bool) {
|
||||
name = FormatMatchingModelName(name)
|
||||
|
||||
if price, ok := modelPriceMap.Get(name); ok {
|
||||
return price, true
|
||||
}
|
||||
|
||||
if strings.HasSuffix(name, CompactModelSuffix) {
|
||||
price, ok := modelPriceMap.Get(CompactWildcardModelKey)
|
||||
if !ok {
|
||||
@@ -372,14 +376,10 @@ func GetModelPrice(name string, printErr bool) (float64, bool) {
|
||||
return price, true
|
||||
}
|
||||
|
||||
price, ok := modelPriceMap.Get(name)
|
||||
if !ok {
|
||||
if printErr {
|
||||
common.SysError("model price not found: " + name)
|
||||
}
|
||||
return -1, false
|
||||
if printErr {
|
||||
common.SysError("model price not found: " + name)
|
||||
}
|
||||
return price, true
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func UpdateModelRatioByJSONString(jsonStr string) error {
|
||||
|
||||
@@ -390,6 +390,12 @@ func ErrOptionWithNoRecordErrorLog() NewAPIErrorOptions {
|
||||
}
|
||||
}
|
||||
|
||||
func ErrOptionWithStatusCode(statusCode int) NewAPIErrorOptions {
|
||||
return func(e *NewAPIError) {
|
||||
e.StatusCode = statusCode
|
||||
}
|
||||
}
|
||||
|
||||
func ErrOptionWithHideErrMsg(replaceStr string) NewAPIErrorOptions {
|
||||
return func(e *NewAPIError) {
|
||||
if common.DebugEnabled {
|
||||
|
||||
Vendored
+5
-5
@@ -10,7 +10,7 @@
|
||||
"@visactor/react-vchart": "~1.8.8",
|
||||
"@visactor/vchart": "~1.8.8",
|
||||
"@visactor/vchart-semi-theme": "~1.8.8",
|
||||
"axios": "1.12.0",
|
||||
"axios": "1.15.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"history": "^5.3.0",
|
||||
@@ -776,7 +776,7 @@
|
||||
|
||||
"autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="],
|
||||
|
||||
"axios": ["axios@1.12.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg=="],
|
||||
"axios": ["axios@1.15.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q=="],
|
||||
|
||||
"babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
|
||||
|
||||
@@ -1104,13 +1104,13 @@
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
||||
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
|
||||
|
||||
"for-in": ["for-in@1.0.2", "", {}, "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
|
||||
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
|
||||
|
||||
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
|
||||
|
||||
@@ -1656,7 +1656,7 @@
|
||||
|
||||
"protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="],
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -10,7 +10,7 @@
|
||||
"@visactor/react-vchart": "~1.8.8",
|
||||
"@visactor/vchart": "~1.8.8",
|
||||
"@visactor/vchart-semi-theme": "~1.8.8",
|
||||
"axios": "1.13.5",
|
||||
"axios": "1.15.0",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"history": "^5.3.0",
|
||||
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { API, showError } from '../../../helpers';
|
||||
import { Empty, Card, Spin, Typography } from '@douyinfe/semi-ui';
|
||||
const { Title } = Typography;
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MarkdownRenderer from '../markdown/MarkdownRenderer';
|
||||
|
||||
// 检查是否为 URL
|
||||
// Check whether content is a URL.
|
||||
const isUrl = (content) => {
|
||||
try {
|
||||
new URL(content.trim());
|
||||
@@ -38,27 +38,23 @@ const isUrl = (content) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 检查是否为 HTML 内容
|
||||
// Check whether content contains HTML.
|
||||
const isHtmlContent = (content) => {
|
||||
if (!content || typeof content !== 'string') return false;
|
||||
|
||||
// 检查是否包含HTML标签
|
||||
const htmlTagRegex = /<\/?[a-z][\s\S]*>/i;
|
||||
return htmlTagRegex.test(content);
|
||||
};
|
||||
|
||||
// 安全地渲染HTML内容
|
||||
// Parse HTML content and extract inline styles.
|
||||
const sanitizeHtml = (html) => {
|
||||
// 创建一个临时元素来解析HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
|
||||
// 提取样式
|
||||
const styles = Array.from(tempDiv.querySelectorAll('style'))
|
||||
.map((style) => style.innerHTML)
|
||||
.join('\n');
|
||||
|
||||
// 提取body内容,如果没有body标签则使用全部内容
|
||||
const bodyContent = tempDiv.querySelector('body');
|
||||
const content = bodyContent ? bodyContent.innerHTML : html;
|
||||
|
||||
@@ -76,15 +72,11 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
||||
const { t } = useTranslation();
|
||||
const [content, setContent] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [htmlStyles, setHtmlStyles] = useState('');
|
||||
const [processedHtmlContent, setProcessedHtmlContent] = useState('');
|
||||
|
||||
const loadContent = async () => {
|
||||
// 先从缓存中获取
|
||||
const cachedContent = localStorage.getItem(cacheKey) || '';
|
||||
if (cachedContent) {
|
||||
setContent(cachedContent);
|
||||
processContent(cachedContent);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -93,7 +85,6 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success && data) {
|
||||
setContent(data);
|
||||
processContent(data);
|
||||
localStorage.setItem(cacheKey, data);
|
||||
} else {
|
||||
if (!cachedContent) {
|
||||
@@ -111,16 +102,12 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const processContent = (rawContent) => {
|
||||
if (isHtmlContent(rawContent)) {
|
||||
const { content: htmlContent, styles } = sanitizeHtml(rawContent);
|
||||
setProcessedHtmlContent(htmlContent);
|
||||
setHtmlStyles(styles);
|
||||
} else {
|
||||
setProcessedHtmlContent('');
|
||||
setHtmlStyles('');
|
||||
const htmlPayload = useMemo(() => {
|
||||
if (!isHtmlContent(content)) {
|
||||
return { content: '', styles: '' };
|
||||
}
|
||||
};
|
||||
return sanitizeHtml(content);
|
||||
}, [content]);
|
||||
|
||||
useEffect(() => {
|
||||
loadContent();
|
||||
@@ -129,8 +116,9 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
||||
// 处理HTML样式注入
|
||||
useEffect(() => {
|
||||
const styleId = `document-renderer-styles-${cacheKey}`;
|
||||
const { styles } = htmlPayload;
|
||||
|
||||
if (htmlStyles) {
|
||||
if (styles) {
|
||||
let styleEl = document.getElementById(styleId);
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement('style');
|
||||
@@ -138,7 +126,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
||||
styleEl.type = 'text/css';
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
styleEl.innerHTML = htmlStyles;
|
||||
styleEl.innerHTML = styles;
|
||||
} else {
|
||||
const el = document.getElementById(styleId);
|
||||
if (el) el.remove();
|
||||
@@ -148,7 +136,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
||||
const el = document.getElementById(styleId);
|
||||
if (el) el.remove();
|
||||
};
|
||||
}, [htmlStyles, cacheKey]);
|
||||
}, [cacheKey, htmlPayload]);
|
||||
|
||||
// 显示加载状态
|
||||
if (loading) {
|
||||
@@ -207,15 +195,6 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
||||
|
||||
// 如果是 HTML 内容,直接渲染
|
||||
if (isHtmlContent(content)) {
|
||||
const { content: htmlContent, styles } = sanitizeHtml(content);
|
||||
|
||||
// 设置样式(如果有的话)
|
||||
useEffect(() => {
|
||||
if (styles && styles !== htmlStyles) {
|
||||
setHtmlStyles(styles);
|
||||
}
|
||||
}, [content, styles, htmlStyles]);
|
||||
|
||||
return (
|
||||
<div className='min-h-screen bg-gray-50'>
|
||||
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
|
||||
@@ -225,7 +204,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
||||
</Title>
|
||||
<div
|
||||
className='prose prose-lg max-w-none'
|
||||
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
||||
dangerouslySetInnerHTML={{ __html: htmlPayload.content }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
|
||||
import React from 'react';
|
||||
import { Card, Avatar, Tag, Divider, Empty } from '@douyinfe/semi-ui';
|
||||
import { Server, Gauge, ExternalLink } from 'lucide-react';
|
||||
import { Server, Gauge, ExternalLink, Copy } from 'lucide-react';
|
||||
import {
|
||||
IllustrationConstruction,
|
||||
IllustrationConstructionDark,
|
||||
@@ -87,11 +87,18 @@ const ApiInfoPanel = ({
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='!text-semi-color-primary break-all cursor-pointer hover:underline mb-1'
|
||||
onClick={() => handleCopyUrl(api.url)}
|
||||
>
|
||||
{api.url}
|
||||
<div className='flex items-center gap-1 mb-1'>
|
||||
<span
|
||||
className='!text-semi-color-primary break-all cursor-pointer hover:underline'
|
||||
onClick={() => handleCopyUrl(api.url)}
|
||||
>
|
||||
{api.url}
|
||||
</span>
|
||||
<Copy
|
||||
size={14}
|
||||
className='flex-shrink-0 text-gray-400 hover:text-semi-color-primary cursor-pointer transition-colors'
|
||||
onClick={() => handleCopyUrl(api.url)}
|
||||
/>
|
||||
</div>
|
||||
<div className='text-gray-500'>{api.description}</div>
|
||||
</div>
|
||||
|
||||
@@ -95,13 +95,15 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon={currentButtonIcon}
|
||||
aria-label={t('切换主题')}
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 !rounded-full !bg-semi-color-fill-0 hover:!bg-semi-color-fill-1'
|
||||
/>
|
||||
<span className='inline-flex'>
|
||||
<Button
|
||||
icon={currentButtonIcon}
|
||||
aria-label={t('切换主题')}
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 !rounded-full !bg-semi-color-fill-0 hover:!bg-semi-color-fill-1'
|
||||
/>
|
||||
</span>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,8 +21,9 @@ import React, { useRef, useEffect } from 'react';
|
||||
import { Typography, TextArea, Button } from '@douyinfe/semi-ui';
|
||||
import MarkdownRenderer from '../common/markdown/MarkdownRenderer';
|
||||
import ThinkingContent from './ThinkingContent';
|
||||
import { Loader2, Check, X } from 'lucide-react';
|
||||
import { Loader2, Check, X, Settings, AlertTriangle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { isAdmin } from '../../helpers/utils';
|
||||
|
||||
const MessageContent = ({
|
||||
message,
|
||||
@@ -64,6 +65,44 @@ const MessageContent = ({
|
||||
errorText = t('请求发生错误');
|
||||
}
|
||||
|
||||
if (message.errorCode === 'model_price_error') {
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<div
|
||||
className='rounded-lg p-3 space-y-2'
|
||||
style={{
|
||||
background: 'var(--semi-color-bg-0)',
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
<AlertTriangle size={16} className='text-orange-500 shrink-0' />
|
||||
<Typography.Text strong className='!text-[var(--semi-color-text-0)]'>
|
||||
{t('模型价格未配置')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Paragraph
|
||||
className='!text-[var(--semi-color-text-1)] !text-sm !mb-0'
|
||||
style={{ wordBreak: 'break-word' }}
|
||||
>
|
||||
{errorText}
|
||||
</Typography.Paragraph>
|
||||
{isAdmin() && (
|
||||
<Button
|
||||
size='small'
|
||||
theme='light'
|
||||
type='warning'
|
||||
icon={<Settings size={14} />}
|
||||
onClick={() => window.open('/console/setting?tab=ratio', '_blank')}
|
||||
>
|
||||
{t('前往设置')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<Typography.Text className='text-white'>{errorText}</Typography.Text>
|
||||
|
||||
@@ -91,6 +91,7 @@ const SystemSetting = () => {
|
||||
EmailDomainRestrictionEnabled: '',
|
||||
EmailAliasRestrictionEnabled: '',
|
||||
SMTPSSLEnabled: '',
|
||||
SMTPForceAuthLogin: '',
|
||||
EmailDomainWhitelist: [],
|
||||
TelegramOAuthEnabled: '',
|
||||
TelegramBotToken: '',
|
||||
@@ -182,6 +183,7 @@ const SystemSetting = () => {
|
||||
case 'EmailDomainRestrictionEnabled':
|
||||
case 'EmailAliasRestrictionEnabled':
|
||||
case 'SMTPSSLEnabled':
|
||||
case 'SMTPForceAuthLogin':
|
||||
case 'LinuxDOOAuthEnabled':
|
||||
case 'discord.enabled':
|
||||
case 'oidc.enabled':
|
||||
@@ -1335,6 +1337,15 @@ const SystemSetting = () => {
|
||||
>
|
||||
{t('启用SMTP SSL')}
|
||||
</Form.Checkbox>
|
||||
<Form.Checkbox
|
||||
field='SMTPForceAuthLogin'
|
||||
noLabel
|
||||
onChange={(e) =>
|
||||
handleCheckboxChange('SMTPForceAuthLogin', e)
|
||||
}
|
||||
>
|
||||
{t('强制使用 AUTH LOGIN')}
|
||||
</Form.Checkbox>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={submitSMTP}>{t('保存 SMTP 设置')}</Button>
|
||||
|
||||
@@ -208,6 +208,7 @@ const EditChannelModal = (props) => {
|
||||
allow_safety_identifier: false,
|
||||
allow_include_obfuscation: false,
|
||||
allow_inference_geo: false,
|
||||
allow_speed: false,
|
||||
claude_beta_query: false,
|
||||
upstream_model_update_check_enabled: false,
|
||||
upstream_model_update_auto_sync_enabled: false,
|
||||
@@ -890,6 +891,7 @@ const EditChannelModal = (props) => {
|
||||
parsedSettings.allow_include_obfuscation || false;
|
||||
data.allow_inference_geo =
|
||||
parsedSettings.allow_inference_geo || false;
|
||||
data.allow_speed = parsedSettings.allow_speed || false;
|
||||
data.claude_beta_query = parsedSettings.claude_beta_query || false;
|
||||
data.upstream_model_update_check_enabled =
|
||||
parsedSettings.upstream_model_update_check_enabled === true;
|
||||
@@ -919,6 +921,7 @@ const EditChannelModal = (props) => {
|
||||
data.allow_safety_identifier = false;
|
||||
data.allow_include_obfuscation = false;
|
||||
data.allow_inference_geo = false;
|
||||
data.allow_speed = false;
|
||||
data.claude_beta_query = false;
|
||||
data.upstream_model_update_check_enabled = false;
|
||||
data.upstream_model_update_auto_sync_enabled = false;
|
||||
@@ -936,6 +939,7 @@ const EditChannelModal = (props) => {
|
||||
data.allow_safety_identifier = false;
|
||||
data.allow_include_obfuscation = false;
|
||||
data.allow_inference_geo = false;
|
||||
data.allow_speed = false;
|
||||
data.claude_beta_query = false;
|
||||
data.upstream_model_update_check_enabled = false;
|
||||
data.upstream_model_update_auto_sync_enabled = false;
|
||||
@@ -1776,6 +1780,7 @@ const EditChannelModal = (props) => {
|
||||
}
|
||||
if (localInputs.type === 14) {
|
||||
settings.allow_inference_geo = localInputs.allow_inference_geo === true;
|
||||
settings.allow_speed = localInputs.allow_speed === true;
|
||||
settings.claude_beta_query = localInputs.claude_beta_query === true;
|
||||
}
|
||||
}
|
||||
@@ -1823,6 +1828,7 @@ const EditChannelModal = (props) => {
|
||||
delete localInputs.allow_safety_identifier;
|
||||
delete localInputs.allow_include_obfuscation;
|
||||
delete localInputs.allow_inference_geo;
|
||||
delete localInputs.allow_speed;
|
||||
delete localInputs.claude_beta_query;
|
||||
delete localInputs.upstream_model_update_check_enabled;
|
||||
delete localInputs.upstream_model_update_auto_sync_enabled;
|
||||
@@ -2480,6 +2486,7 @@ const EditChannelModal = (props) => {
|
||||
</div>
|
||||
<Form.Switch field='allow_service_tier' label={t('允许 service_tier 透传')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelOtherSettingsChange('allow_service_tier', value)} extraText={t('service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用')} />
|
||||
<Form.Switch field='allow_inference_geo' label={t('允许 inference_geo 透传')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelOtherSettingsChange('allow_inference_geo', value)} extraText={t('inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息')} />
|
||||
<Form.Switch field='allow_speed' label={t('允许 speed 透传')} checkedText={t('开')} uncheckedText={t('关')} onChange={(value) => handleChannelOtherSettingsChange('allow_speed', value)} extraText={t('speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式')} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
Banner,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IconSearch, IconInfoCircle } from '@douyinfe/semi-icons';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { copy, showError, showInfo, showSuccess } from '../../../../helpers';
|
||||
import { MODEL_TABLE_PAGE_SIZE } from '../../../../constants';
|
||||
|
||||
@@ -168,17 +169,43 @@ const ModelTestModal = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Tag color={testResult.success ? 'green' : 'red'} shape='circle'>
|
||||
{testResult.success ? t('成功') : t('失败')}
|
||||
</Tag>
|
||||
{testResult.success && (
|
||||
<Typography.Text type='tertiary'>
|
||||
{t('请求时长: ${time}s').replace(
|
||||
'${time}',
|
||||
testResult.time.toFixed(2),
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Tag color={testResult.success ? 'green' : 'red'} shape='circle'>
|
||||
{testResult.success ? t('成功') : t('失败')}
|
||||
</Tag>
|
||||
{testResult.success && (
|
||||
<Typography.Text type='tertiary'>
|
||||
{t('请求时长: ${time}s').replace(
|
||||
'${time}',
|
||||
testResult.time.toFixed(2),
|
||||
)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
{!testResult.success && testResult.message && (
|
||||
<div className='flex flex-col gap-1'>
|
||||
<Typography.Text
|
||||
type='danger'
|
||||
size='small'
|
||||
className='break-all'
|
||||
style={{ maxWidth: '400px', fontSize: '12px' }}
|
||||
>
|
||||
{testResult.message}
|
||||
</Typography.Text>
|
||||
{testResult.errorCode === 'model_price_error' && (
|
||||
<Button
|
||||
size='small'
|
||||
theme='light'
|
||||
type='warning'
|
||||
icon={<Settings size={12} />}
|
||||
onClick={() => window.open('/console/setting?tab=ratio', '_blank')}
|
||||
style={{ width: 'fit-content' }}
|
||||
>
|
||||
{t('前往设置')}
|
||||
</Button>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -360,7 +360,7 @@ const MultiKeyManageModal = ({ visible, onCancel, channel, onRefresh }) => {
|
||||
{
|
||||
title: t('索引'),
|
||||
dataIndex: 'index',
|
||||
render: (text) => `#${text}`,
|
||||
render: (text) => `#${Number(text) + 1}`,
|
||||
},
|
||||
// {
|
||||
// title: t('密钥预览'),
|
||||
|
||||
@@ -25,8 +25,12 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
renderQuota,
|
||||
renderQuotaWithPrompt,
|
||||
getCurrencyConfig,
|
||||
} from '../../../../helpers';
|
||||
import {
|
||||
quotaToDisplayAmount,
|
||||
displayAmountToQuota,
|
||||
} from '../../../../helpers/quota';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
import {
|
||||
Button,
|
||||
@@ -41,6 +45,7 @@ import {
|
||||
Avatar,
|
||||
Row,
|
||||
Col,
|
||||
InputNumber,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconCreditCard,
|
||||
@@ -57,10 +62,12 @@ const EditRedemptionModal = (props) => {
|
||||
const [loading, setLoading] = useState(isEdit);
|
||||
const isMobile = useIsMobile();
|
||||
const formApiRef = useRef(null);
|
||||
const [showQuotaInput, setShowQuotaInput] = useState(false);
|
||||
|
||||
const getInitValues = () => ({
|
||||
name: '',
|
||||
quota: 100000,
|
||||
amount: Number(quotaToDisplayAmount(100000).toFixed(6)),
|
||||
count: 1,
|
||||
expired_time: null,
|
||||
});
|
||||
@@ -79,6 +86,7 @@ const EditRedemptionModal = (props) => {
|
||||
} else {
|
||||
data.expired_time = new Date(data.expired_time * 1000);
|
||||
}
|
||||
data.amount = Number(quotaToDisplayAmount(data.quota || 0).toFixed(6));
|
||||
formApiRef.current?.setValues({ ...getInitValues(), ...data });
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -104,7 +112,12 @@ const EditRedemptionModal = (props) => {
|
||||
setLoading(true);
|
||||
let localInputs = { ...values };
|
||||
localInputs.count = parseInt(localInputs.count) || 0;
|
||||
localInputs.quota = parseInt(localInputs.quota) || 0;
|
||||
localInputs.quota = displayAmountToQuota(localInputs.amount);
|
||||
if (localInputs.quota <= 0) {
|
||||
showError(t('请输入金额'));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
localInputs.name = name;
|
||||
if (!localInputs.expired_time) {
|
||||
localInputs.expired_time = 0;
|
||||
@@ -285,37 +298,63 @@ const EditRedemptionModal = (props) => {
|
||||
</div>
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<Form.AutoComplete
|
||||
field='quota'
|
||||
label={t('额度')}
|
||||
placeholder={t('请输入额度')}
|
||||
<Col span={24}>
|
||||
<Form.InputNumber
|
||||
field='amount'
|
||||
label={t('金额')}
|
||||
prefix={getCurrencyConfig().symbol}
|
||||
placeholder={t('输入金额')}
|
||||
precision={6}
|
||||
min={0}
|
||||
step={0.000001}
|
||||
style={{ width: '100%' }}
|
||||
type='number'
|
||||
rules={[
|
||||
{ required: true, message: t('请输入额度') },
|
||||
{
|
||||
validator: (rule, v) => {
|
||||
const num = parseInt(v, 10);
|
||||
return num > 0
|
||||
? Promise.resolve()
|
||||
: Promise.reject(t('额度必须大于0'));
|
||||
},
|
||||
},
|
||||
]}
|
||||
extraText={renderQuotaWithPrompt(
|
||||
Number(values.quota) || 0,
|
||||
)}
|
||||
data={[
|
||||
{ value: 500000, label: '1$' },
|
||||
{ value: 5000000, label: '10$' },
|
||||
{ value: 25000000, label: '50$' },
|
||||
{ value: 50000000, label: '100$' },
|
||||
{ value: 250000000, label: '500$' },
|
||||
{ value: 500000000, label: '1000$' },
|
||||
]}
|
||||
onChange={(val) => {
|
||||
const amount = val === '' || val == null ? 0 : val;
|
||||
formApiRef.current?.setValue('amount', amount);
|
||||
formApiRef.current?.setValue(
|
||||
'quota',
|
||||
displayAmountToQuota(amount),
|
||||
);
|
||||
}}
|
||||
showClear
|
||||
/>
|
||||
<div
|
||||
className='text-xs cursor-pointer mt-1'
|
||||
style={{ color: 'var(--semi-color-text-2)' }}
|
||||
onClick={() => setShowQuotaInput((v) => !v)}
|
||||
>
|
||||
{showQuotaInput
|
||||
? `▾ ${t('收起原生额度输入')}`
|
||||
: `▸ ${t('使用原生额度输入')}`}
|
||||
</div>
|
||||
<div style={{ display: showQuotaInput ? 'block' : 'none' }} className='mt-2'>
|
||||
<Form.InputNumber
|
||||
field='quota'
|
||||
label={t('额度')}
|
||||
placeholder={t('输入额度')}
|
||||
rules={[
|
||||
{ required: true, message: t('请输入额度') },
|
||||
{
|
||||
validator: (rule, v) => {
|
||||
const num = parseInt(v, 10);
|
||||
return num > 0
|
||||
? Promise.resolve()
|
||||
: Promise.reject(t('额度必须大于0'));
|
||||
},
|
||||
},
|
||||
]}
|
||||
onChange={(val) => {
|
||||
const quota = val === '' || val == null ? 0 : val;
|
||||
formApiRef.current?.setValue('quota', quota);
|
||||
formApiRef.current?.setValue(
|
||||
'amount',
|
||||
Number(quotaToDisplayAmount(quota).toFixed(6)),
|
||||
);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
{!isEdit && (
|
||||
<Col span={12}>
|
||||
|
||||
@@ -24,10 +24,14 @@ import {
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
renderGroupOption,
|
||||
renderQuotaWithPrompt,
|
||||
getCurrencyConfig,
|
||||
getModelCategories,
|
||||
selectFilter,
|
||||
} from '../../../../helpers';
|
||||
import {
|
||||
quotaToDisplayAmount,
|
||||
displayAmountToQuota,
|
||||
} from '../../../../helpers/quota';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
import {
|
||||
Button,
|
||||
@@ -41,6 +45,7 @@ import {
|
||||
Form,
|
||||
Col,
|
||||
Row,
|
||||
InputNumber,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconCreditCard,
|
||||
@@ -62,11 +67,13 @@ const EditTokenModal = (props) => {
|
||||
const formApiRef = useRef(null);
|
||||
const [models, setModels] = useState([]);
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [showQuotaInput, setShowQuotaInput] = useState(false);
|
||||
const isEdit = props.editingToken.id !== undefined;
|
||||
|
||||
const getInitValues = () => ({
|
||||
name: '',
|
||||
remain_quota: 0,
|
||||
remain_amount: 0,
|
||||
expired_time: -1,
|
||||
unlimited_quota: true,
|
||||
model_limits_enabled: false,
|
||||
@@ -162,6 +169,9 @@ const EditTokenModal = (props) => {
|
||||
} else {
|
||||
data.model_limits = [];
|
||||
}
|
||||
data.remain_amount = Number(
|
||||
quotaToDisplayAmount(data.remain_quota || 0).toFixed(6),
|
||||
);
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValues({ ...getInitValues(), ...data });
|
||||
}
|
||||
@@ -209,7 +219,14 @@ const EditTokenModal = (props) => {
|
||||
setLoading(true);
|
||||
if (isEdit) {
|
||||
let { tokenCount: _tc, ...localInputs } = values;
|
||||
localInputs.remain_quota = parseInt(localInputs.remain_quota);
|
||||
localInputs.remain_quota = localInputs.unlimited_quota
|
||||
? 0
|
||||
: displayAmountToQuota(localInputs.remain_amount);
|
||||
if (!localInputs.unlimited_quota && localInputs.remain_quota <= 0) {
|
||||
showError(t('请输入金额'));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (localInputs.expired_time !== -1) {
|
||||
let time = Date.parse(localInputs.expired_time);
|
||||
if (isNaN(time)) {
|
||||
@@ -245,7 +262,14 @@ const EditTokenModal = (props) => {
|
||||
} else {
|
||||
localInputs.name = baseName;
|
||||
}
|
||||
localInputs.remain_quota = parseInt(localInputs.remain_quota);
|
||||
localInputs.remain_quota = localInputs.unlimited_quota
|
||||
? 0
|
||||
: displayAmountToQuota(localInputs.remain_amount);
|
||||
if (!localInputs.unlimited_quota && localInputs.remain_quota <= 0) {
|
||||
showError(t('请输入金额'));
|
||||
setLoading(false);
|
||||
break;
|
||||
}
|
||||
|
||||
if (localInputs.expired_time !== -1) {
|
||||
let time = Date.parse(localInputs.expired_time);
|
||||
@@ -497,28 +521,63 @@ const EditTokenModal = (props) => {
|
||||
</div>
|
||||
<Row gutter={12}>
|
||||
<Col span={24}>
|
||||
<Form.AutoComplete
|
||||
field='remain_quota'
|
||||
label={t('额度')}
|
||||
placeholder={t('请输入额度')}
|
||||
type='number'
|
||||
<Form.InputNumber
|
||||
field='remain_amount'
|
||||
label={t('金额')}
|
||||
prefix={getCurrencyConfig().symbol}
|
||||
placeholder={t('输入金额')}
|
||||
precision={6}
|
||||
disabled={values.unlimited_quota}
|
||||
extraText={renderQuotaWithPrompt(values.remain_quota)}
|
||||
rules={
|
||||
values.unlimited_quota
|
||||
? []
|
||||
: [{ required: true, message: t('请输入额度') }]
|
||||
}
|
||||
data={[
|
||||
{ value: 500000, label: '1$' },
|
||||
{ value: 5000000, label: '10$' },
|
||||
{ value: 25000000, label: '50$' },
|
||||
{ value: 50000000, label: '100$' },
|
||||
{ value: 250000000, label: '500$' },
|
||||
{ value: 500000000, label: '1000$' },
|
||||
]}
|
||||
min={0}
|
||||
step={0.000001}
|
||||
onChange={(val) => {
|
||||
const amount = val === '' || val == null ? 0 : val;
|
||||
formApiRef.current?.setValue('remain_amount', amount);
|
||||
formApiRef.current?.setValue(
|
||||
'remain_quota',
|
||||
displayAmountToQuota(amount),
|
||||
);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
showClear
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<div
|
||||
className='text-xs cursor-pointer mt-1'
|
||||
style={{ color: 'var(--semi-color-text-2)' }}
|
||||
onClick={() => setShowQuotaInput((v) => !v)}
|
||||
>
|
||||
{showQuotaInput
|
||||
? `▾ ${t('收起原生额度输入')}`
|
||||
: `▸ ${t('使用原生额度输入')}`}
|
||||
</div>
|
||||
<div style={{ display: showQuotaInput ? 'block' : 'none' }} className='mt-2'>
|
||||
<Form.InputNumber
|
||||
field='remain_quota'
|
||||
label={t('额度')}
|
||||
placeholder={t('输入额度')}
|
||||
disabled={values.unlimited_quota}
|
||||
min={0}
|
||||
step={500000}
|
||||
rules={
|
||||
values.unlimited_quota
|
||||
? []
|
||||
: [{ required: true, message: t('请输入额度') }]
|
||||
}
|
||||
onChange={(val) => {
|
||||
const quota = val === '' || val == null ? 0 : val;
|
||||
formApiRef.current?.setValue('remain_quota', quota);
|
||||
formApiRef.current?.setValue(
|
||||
'remain_amount',
|
||||
Number(quotaToDisplayAmount(quota).toFixed(6)),
|
||||
);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Switch
|
||||
field='unlimited_quota'
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
renderQuota,
|
||||
renderQuotaWithPrompt,
|
||||
getCurrencyConfig,
|
||||
} from '../../../../helpers';
|
||||
import {
|
||||
@@ -46,6 +45,8 @@ import {
|
||||
Row,
|
||||
Col,
|
||||
InputNumber,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconUser,
|
||||
@@ -53,7 +54,7 @@ import {
|
||||
IconClose,
|
||||
IconLink,
|
||||
IconUserGroup,
|
||||
IconPlus,
|
||||
IconEdit,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import UserBindingManagementModal from './UserBindingManagementModal';
|
||||
|
||||
@@ -63,13 +64,18 @@ const EditUserModal = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const userId = props.editingUser.id;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [addQuotaModalOpen, setIsModalOpen] = useState(false);
|
||||
const [addQuotaLocal, setAddQuotaLocal] = useState('');
|
||||
const [addAmountLocal, setAddAmountLocal] = useState('');
|
||||
const [adjustModalOpen, setAdjustModalOpen] = useState(false);
|
||||
const [adjustQuotaLocal, setAdjustQuotaLocal] = useState('');
|
||||
const [adjustAmountLocal, setAdjustAmountLocal] = useState('');
|
||||
const [adjustMode, setAdjustMode] = useState('add');
|
||||
const [adjustLoading, setAdjustLoading] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
const [groupOptions, setGroupOptions] = useState([]);
|
||||
const [bindingModalVisible, setBindingModalVisible] = useState(false);
|
||||
const formApiRef = useRef(null);
|
||||
const [showAdjustQuotaRaw, setShowAdjustQuotaRaw] = useState(false);
|
||||
const [showQuotaInput, setShowQuotaInput] = useState(false);
|
||||
const [inputs, setInputs] = useState(null);
|
||||
|
||||
const isEdit = Boolean(userId);
|
||||
|
||||
@@ -85,6 +91,7 @@ const EditUserModal = (props) => {
|
||||
linux_do_id: '',
|
||||
email: '',
|
||||
quota: 0,
|
||||
quota_amount: 0,
|
||||
group: 'default',
|
||||
remark: '',
|
||||
});
|
||||
@@ -107,13 +114,22 @@ const EditUserModal = (props) => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
data.password = '';
|
||||
formApiRef.current?.setValues({ ...getInitValues(), ...data });
|
||||
data.quota_amount = Number(
|
||||
quotaToDisplayAmount(data.quota || 0).toFixed(6),
|
||||
);
|
||||
setInputs({ ...getInitValues(), ...data });
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (inputs && formApiRef.current) {
|
||||
formApiRef.current.setValues(inputs);
|
||||
}
|
||||
}, [inputs]);
|
||||
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
if (userId) fetchGroups();
|
||||
@@ -132,8 +148,8 @@ const EditUserModal = (props) => {
|
||||
const submit = async (values) => {
|
||||
setLoading(true);
|
||||
let payload = { ...values };
|
||||
if (typeof payload.quota === 'string')
|
||||
payload.quota = parseInt(payload.quota) || 0;
|
||||
delete payload.quota;
|
||||
delete payload.quota_amount;
|
||||
if (userId) {
|
||||
payload.id = parseInt(userId);
|
||||
}
|
||||
@@ -150,11 +166,60 @@ const EditUserModal = (props) => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
/* --------------------- quota helper -------------------- */
|
||||
const addLocalQuota = () => {
|
||||
const current = parseInt(formApiRef.current?.getValue('quota') || 0);
|
||||
const delta = parseInt(addQuotaLocal) || 0;
|
||||
formApiRef.current?.setValue('quota', current + delta);
|
||||
/* --------------------- atomic quota adjust -------------------- */
|
||||
const adjustQuota = async () => {
|
||||
const quotaVal = parseInt(adjustQuotaLocal) || 0;
|
||||
if (quotaVal <= 0 && adjustMode !== 'override') return;
|
||||
if (adjustMode === 'override' && (adjustQuotaLocal === '' || adjustQuotaLocal == null)) return;
|
||||
setAdjustLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/user/manage', {
|
||||
id: parseInt(userId),
|
||||
action: 'add_quota',
|
||||
mode: adjustMode,
|
||||
value: adjustMode === 'override' ? quotaVal : Math.abs(quotaVal),
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess(t('调整额度成功'));
|
||||
setAdjustModalOpen(false);
|
||||
setAdjustQuotaLocal('');
|
||||
setAdjustAmountLocal('');
|
||||
const userRes = await API.get(`/api/user/${userId}`);
|
||||
if (userRes.data.success) {
|
||||
const data = userRes.data.data;
|
||||
data.password = '';
|
||||
data.quota_amount = Number(
|
||||
quotaToDisplayAmount(data.quota || 0).toFixed(6),
|
||||
);
|
||||
setInputs({ ...getInitValues(), ...data });
|
||||
}
|
||||
props.refresh();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (e) {
|
||||
showError(e.message);
|
||||
}
|
||||
setAdjustLoading(false);
|
||||
};
|
||||
|
||||
const getPreviewText = () => {
|
||||
const current = formApiRef.current?.getValue('quota') || 0;
|
||||
const val = parseInt(adjustQuotaLocal) || 0;
|
||||
let result;
|
||||
switch (adjustMode) {
|
||||
case 'add':
|
||||
result = current + Math.abs(val);
|
||||
return `${t('当前额度')}:${renderQuota(current)},+${renderQuota(Math.abs(val))} = ${renderQuota(result)}`;
|
||||
case 'subtract':
|
||||
result = current - Math.abs(val);
|
||||
return `${t('当前额度')}:${renderQuota(current)},-${renderQuota(Math.abs(val))} = ${renderQuota(result)}`;
|
||||
case 'override':
|
||||
return `${t('当前额度')}:${renderQuota(current)} → ${renderQuota(val)}`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
/* --------------------------- UI --------------------------- */
|
||||
@@ -305,24 +370,47 @@ const EditUserModal = (props) => {
|
||||
|
||||
<Col span={10}>
|
||||
<Form.InputNumber
|
||||
field='quota'
|
||||
label={t('剩余额度')}
|
||||
placeholder={t('请输入新的剩余额度')}
|
||||
step={500000}
|
||||
extraText={renderQuotaWithPrompt(values.quota || 0)}
|
||||
rules={[{ required: true, message: t('请输入额度') }]}
|
||||
field='quota_amount'
|
||||
label={t('金额')}
|
||||
prefix={getCurrencyConfig().symbol}
|
||||
precision={6}
|
||||
step={0.000001}
|
||||
style={{ width: '100%' }}
|
||||
readonly
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col span={14}>
|
||||
<Form.Slot label={t('添加额度')}>
|
||||
<Form.Slot label={t('调整额度')}>
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
/>
|
||||
icon={<IconEdit />}
|
||||
onClick={() => setAdjustModalOpen(true)}
|
||||
>
|
||||
{t('调整额度')}
|
||||
</Button>
|
||||
</Form.Slot>
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<div
|
||||
className='text-xs cursor-pointer'
|
||||
style={{ color: 'var(--semi-color-text-2)' }}
|
||||
onClick={() => setShowQuotaInput((v) => !v)}
|
||||
>
|
||||
{showQuotaInput
|
||||
? `▾ ${t('收起原生额度输入')}`
|
||||
: `▸ ${t('使用原生额度输入')}`}
|
||||
</div>
|
||||
<div style={{ display: showQuotaInput ? 'block' : 'none' }} className='mt-2'>
|
||||
<Form.InputNumber
|
||||
field='quota'
|
||||
label={t('额度')}
|
||||
placeholder={t('请输入额度')}
|
||||
style={{ width: '100%' }}
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
@@ -372,81 +460,102 @@ const EditUserModal = (props) => {
|
||||
formApiRef={formApiRef}
|
||||
/>
|
||||
|
||||
{/* 添加额度模态框 */}
|
||||
{/* 调整额度模态框 */}
|
||||
<Modal
|
||||
centered
|
||||
visible={addQuotaModalOpen}
|
||||
onOk={() => {
|
||||
addLocalQuota();
|
||||
setIsModalOpen(false);
|
||||
setAddQuotaLocal('');
|
||||
setAddAmountLocal('');
|
||||
}}
|
||||
visible={adjustModalOpen}
|
||||
onOk={adjustQuota}
|
||||
onCancel={() => {
|
||||
setIsModalOpen(false);
|
||||
setAdjustModalOpen(false);
|
||||
setAdjustQuotaLocal('');
|
||||
setAdjustAmountLocal('');
|
||||
setAdjustMode('add');
|
||||
}}
|
||||
confirmLoading={adjustLoading}
|
||||
closable={null}
|
||||
title={
|
||||
<div className='flex items-center'>
|
||||
<IconPlus className='mr-2' />
|
||||
{t('添加额度')}
|
||||
<IconEdit className='mr-2' />
|
||||
{t('调整额度')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className='mb-4'>
|
||||
{(() => {
|
||||
const current = formApiRef.current?.getValue('quota') || 0;
|
||||
return (
|
||||
<Text type='secondary' className='block mb-2'>
|
||||
{`${t('新额度:')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`}
|
||||
</Text>
|
||||
);
|
||||
})()}
|
||||
<Text type='secondary' className='block mb-2'>
|
||||
{getPreviewText()}
|
||||
</Text>
|
||||
</div>
|
||||
{getCurrencyConfig().type !== 'TOKENS' && (
|
||||
<div className='mb-3'>
|
||||
<div className='mb-1'>
|
||||
<Text size='small'>{t('金额')}</Text>
|
||||
<Text size='small' type='tertiary'>
|
||||
{' '}
|
||||
({t('仅用于换算,实际保存的是额度')})
|
||||
</Text>
|
||||
</div>
|
||||
<InputNumber
|
||||
prefix={getCurrencyConfig().symbol}
|
||||
placeholder={t('输入金额')}
|
||||
value={addAmountLocal}
|
||||
precision={2}
|
||||
onChange={(val) => {
|
||||
setAddAmountLocal(val);
|
||||
setAddQuotaLocal(
|
||||
val != null && val !== ''
|
||||
? displayAmountToQuota(Math.abs(val)) * Math.sign(val)
|
||||
: '',
|
||||
);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
showClear
|
||||
/>
|
||||
<div className='mb-3'>
|
||||
<div className='mb-1'>
|
||||
<Text size='small'>{t('操作')}</Text>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<RadioGroup
|
||||
type='button'
|
||||
value={adjustMode}
|
||||
onChange={(e) => {
|
||||
setAdjustMode(e.target.value);
|
||||
setAdjustQuotaLocal('');
|
||||
setAdjustAmountLocal('');
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Radio value='add'>{t('添加')}</Radio>
|
||||
<Radio value='subtract'>{t('减少')}</Radio>
|
||||
<Radio value='override'>{t('覆盖')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className='mb-3'>
|
||||
<div className='mb-1'>
|
||||
<Text size='small'>{t('金额')}</Text>
|
||||
</div>
|
||||
<InputNumber
|
||||
prefix={getCurrencyConfig().symbol}
|
||||
placeholder={t('输入金额')}
|
||||
value={adjustAmountLocal}
|
||||
precision={6}
|
||||
min={adjustMode === 'override' ? undefined : 0}
|
||||
step={0.000001}
|
||||
onChange={(val) => {
|
||||
const amount = val === '' || val == null ? '' : val;
|
||||
setAdjustAmountLocal(amount);
|
||||
setAdjustQuotaLocal(
|
||||
amount === ''
|
||||
? ''
|
||||
: adjustMode === 'override'
|
||||
? displayAmountToQuota(amount)
|
||||
: displayAmountToQuota(Math.abs(amount)),
|
||||
);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
showClear
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className='text-xs cursor-pointer mt-2'
|
||||
style={{ color: 'var(--semi-color-text-2)' }}
|
||||
onClick={() => setShowAdjustQuotaRaw((v) => !v)}
|
||||
>
|
||||
{showAdjustQuotaRaw
|
||||
? `▾ ${t('收起原生额度输入')}`
|
||||
: `▸ ${t('使用原生额度输入')}`}
|
||||
</div>
|
||||
<div style={{ display: showAdjustQuotaRaw ? 'block' : 'none' }} className='mt-2'>
|
||||
<div className='mb-1'>
|
||||
<Text size='small'>{t('额度')}</Text>
|
||||
</div>
|
||||
<InputNumber
|
||||
placeholder={t('输入额度')}
|
||||
value={addQuotaLocal}
|
||||
value={adjustQuotaLocal}
|
||||
min={adjustMode === 'override' ? undefined : 0}
|
||||
onChange={(val) => {
|
||||
setAddQuotaLocal(val);
|
||||
setAddAmountLocal(
|
||||
val != null && val !== ''
|
||||
? Number(
|
||||
(
|
||||
quotaToDisplayAmount(Math.abs(val)) * Math.sign(val)
|
||||
).toFixed(2),
|
||||
)
|
||||
: '',
|
||||
const quota = val === '' || val == null ? '' : val;
|
||||
setAdjustQuotaLocal(quota);
|
||||
setAdjustAmountLocal(
|
||||
quota === ''
|
||||
? ''
|
||||
: adjustMode === 'override'
|
||||
? Number(quotaToDisplayAmount(quota).toFixed(6))
|
||||
: Number(quotaToDisplayAmount(Math.abs(quota)).toFixed(6)),
|
||||
);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
|
||||
@@ -442,6 +442,14 @@ const SubscriptionPlansCard = ({
|
||||
(subscription?.end_time || 0) * 1000,
|
||||
).toLocaleString()}
|
||||
</div>
|
||||
{isActive && subscription?.next_reset_time > 0 && (
|
||||
<div className='text-xs text-gray-500 mb-2'>
|
||||
{t('下一次重置')}:{' '}
|
||||
{new Date(
|
||||
subscription.next_reset_time * 1000,
|
||||
).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
<div className='text-xs text-gray-500 mb-2'>
|
||||
{t('总额度')}:{' '}
|
||||
{totalAmount > 0 ? (
|
||||
|
||||
Vendored
+29
-7
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { getCurrencyConfig } from './render';
|
||||
|
||||
export const getQuotaPerUnit = () => {
|
||||
@@ -7,19 +25,23 @@ export const getQuotaPerUnit = () => {
|
||||
|
||||
export const quotaToDisplayAmount = (quota) => {
|
||||
const q = Number(quota || 0);
|
||||
if (!Number.isFinite(q) || q <= 0) return 0;
|
||||
if (!Number.isFinite(q) || q === 0) return 0;
|
||||
const sign = Math.sign(q);
|
||||
const abs = Math.abs(q);
|
||||
const { type, rate } = getCurrencyConfig();
|
||||
if (type === 'TOKENS') return q;
|
||||
const usd = q / getQuotaPerUnit();
|
||||
if (type === 'USD') return usd;
|
||||
return usd * (rate || 1);
|
||||
const usd = abs / getQuotaPerUnit();
|
||||
if (type === 'USD') return sign * usd;
|
||||
return sign * usd * (rate || 1);
|
||||
};
|
||||
|
||||
export const displayAmountToQuota = (amount) => {
|
||||
const val = Number(amount || 0);
|
||||
if (!Number.isFinite(val) || val <= 0) return 0;
|
||||
if (!Number.isFinite(val) || val === 0) return 0;
|
||||
const sign = Math.sign(val);
|
||||
const abs = Math.abs(val);
|
||||
const { type, rate } = getCurrencyConfig();
|
||||
if (type === 'TOKENS') return Math.round(val);
|
||||
const usd = type === 'USD' ? val : val / (rate || 1);
|
||||
return Math.round(usd * getQuotaPerUnit());
|
||||
const usd = type === 'USD' ? abs : abs / (rate || 1);
|
||||
return sign * Math.round(usd * getQuotaPerUnit());
|
||||
};
|
||||
|
||||
+5
-3
@@ -890,7 +890,7 @@ export const useChannelsData = () => {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const { success, message, time } = res.data;
|
||||
const { success, message, time, error_code } = res.data;
|
||||
|
||||
// 更新测试结果
|
||||
setModelTestResults((prev) => ({
|
||||
@@ -900,6 +900,7 @@ export const useChannelsData = () => {
|
||||
message,
|
||||
time: time || 0,
|
||||
timestamp: Date.now(),
|
||||
errorCode: error_code || null,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -927,7 +928,7 @@ export const useChannelsData = () => {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showError(`${t('模型')} ${model}: ${message}`);
|
||||
showError(message);
|
||||
}
|
||||
} catch (error) {
|
||||
// 处理网络错误
|
||||
@@ -939,9 +940,10 @@ export const useChannelsData = () => {
|
||||
message: error.message || t('网络错误'),
|
||||
time: 0,
|
||||
timestamp: Date.now(),
|
||||
errorCode: null,
|
||||
},
|
||||
}));
|
||||
showError(`${t('模型')} ${model}: ${error.message || t('测试失败')}`);
|
||||
showError(error.message || t('测试失败'));
|
||||
} finally {
|
||||
// 从正在测试的模型集合中移除
|
||||
setTestingModels((prev) => {
|
||||
|
||||
+64
-2
@@ -214,6 +214,29 @@ export const useDashboardCharts = (
|
||||
},
|
||||
],
|
||||
},
|
||||
dimension: {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['Model'],
|
||||
value: (datum) => datum['Count'] || 0,
|
||||
},
|
||||
],
|
||||
updateContent: (array) => {
|
||||
array.sort((a, b) => b.value - a.value);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
let value = parseFloat(array[i].value);
|
||||
if (isNaN(value)) value = 0;
|
||||
sum += value;
|
||||
array[i].value = renderNumber(value);
|
||||
}
|
||||
array.unshift({
|
||||
key: t('总计'),
|
||||
value: renderNumber(sum),
|
||||
});
|
||||
return array;
|
||||
},
|
||||
},
|
||||
},
|
||||
color: {
|
||||
specified: modelColorMap,
|
||||
@@ -319,6 +342,12 @@ export const useDashboardCharts = (
|
||||
text: t('用户消耗趋势'),
|
||||
subtext: '',
|
||||
},
|
||||
axes: [{
|
||||
orient: 'left',
|
||||
label: {
|
||||
formatMethod: (value) => renderQuota(value, 2),
|
||||
},
|
||||
}],
|
||||
area: { style: { fillOpacity: 0.15 } },
|
||||
line: { style: { lineWidth: 2 } },
|
||||
point: { visible: false },
|
||||
@@ -329,6 +358,27 @@ export const useDashboardCharts = (
|
||||
value: (datum) => renderQuota(datum['rawQuota'] || 0, 4),
|
||||
}],
|
||||
},
|
||||
dimension: {
|
||||
content: [{
|
||||
key: (datum) => datum['User'],
|
||||
value: (datum) => datum['rawQuota'] || 0,
|
||||
}],
|
||||
updateContent: (array) => {
|
||||
array.sort((a, b) => b.value - a.value);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
let value = parseFloat(array[i].value);
|
||||
if (isNaN(value)) value = 0;
|
||||
sum += value;
|
||||
array[i].value = renderQuota(value, 4);
|
||||
}
|
||||
array.unshift({
|
||||
key: t('总计'),
|
||||
value: renderQuota(sum, 4),
|
||||
});
|
||||
return array;
|
||||
},
|
||||
},
|
||||
},
|
||||
color: { type: 'ordinal', range: USER_COLORS },
|
||||
});
|
||||
@@ -457,13 +507,25 @@ export const useDashboardCharts = (
|
||||
modelLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
||||
|
||||
// ===== 模型调用次数排行柱状图 =====
|
||||
const rankData = Array.from(modelTotals)
|
||||
const MAX_RANK_MODELS = 20;
|
||||
const allRankData = Array.from(modelTotals)
|
||||
.map(([model, count]) => ({
|
||||
Model: model,
|
||||
Count: count,
|
||||
}))
|
||||
.sort((a, b) => b.Count - a.Count);
|
||||
|
||||
let rankData;
|
||||
if (allRankData.length > MAX_RANK_MODELS) {
|
||||
const topModels = allRankData.slice(0, MAX_RANK_MODELS);
|
||||
const otherCount = allRankData
|
||||
.slice(MAX_RANK_MODELS)
|
||||
.reduce((sum, item) => sum + item.Count, 0);
|
||||
rankData = [...topModels, { Model: t('其他'), Count: otherCount }];
|
||||
} else {
|
||||
rankData = allRankData;
|
||||
}
|
||||
|
||||
updateChartSpec(
|
||||
setSpecModelLine,
|
||||
modelLineData,
|
||||
@@ -513,7 +575,7 @@ export const useDashboardCharts = (
|
||||
User: item.User,
|
||||
rawQuota: item.Quota,
|
||||
Quota: getQuotaWithUnit(item.Quota, 4),
|
||||
})).sort((a, b) => a.rawQuota - b.rawQuota);
|
||||
})).sort((a, b) => b.rawQuota - a.rawQuota);
|
||||
|
||||
const totalUserQuota = rankingData.reduce((s, i) => s + i.Quota, 0);
|
||||
|
||||
|
||||
+43
-6
@@ -196,10 +196,17 @@ export const useApiRequest = (
|
||||
|
||||
if (!response.ok) {
|
||||
let errorBody = '';
|
||||
let parsedError = null;
|
||||
try {
|
||||
errorBody = await response.text();
|
||||
const errorJson = JSON.parse(errorBody);
|
||||
if (errorJson?.error) {
|
||||
parsedError = errorJson.error;
|
||||
}
|
||||
} catch (e) {
|
||||
errorBody = '无法读取错误响应体';
|
||||
if (!errorBody) {
|
||||
errorBody = '无法读取错误响应体';
|
||||
}
|
||||
}
|
||||
|
||||
const errorInfo = handleApiError(
|
||||
@@ -215,9 +222,13 @@ export const useApiRequest = (
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
throw new Error(
|
||||
`HTTP error! status: ${response.status}, body: ${errorBody}`,
|
||||
const err = new Error(
|
||||
parsedError?.message ||
|
||||
`HTTP error! status: ${response.status}, body: ${errorBody}`,
|
||||
);
|
||||
err.errorCode = parsedError?.code || null;
|
||||
err.errorType = parsedError?.type || null;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@@ -277,6 +288,7 @@ export const useApiRequest = (
|
||||
newMessages[newMessages.length - 1] = {
|
||||
...lastMessage,
|
||||
content: t('请求发生错误: ') + error.message,
|
||||
errorCode: error.errorCode || null,
|
||||
status: MESSAGE_STATUS.ERROR,
|
||||
...autoCollapseState,
|
||||
};
|
||||
@@ -379,7 +391,20 @@ export const useApiRequest = (
|
||||
// 只有在流没有正常完成且连接状态异常时才处理错误
|
||||
if (!isStreamComplete && source.readyState !== 2) {
|
||||
console.error('SSE Error:', e);
|
||||
const errorMessage = e.data || t('请求发生错误');
|
||||
let errorMessage = e.data || t('请求发生错误');
|
||||
let errorCode = null;
|
||||
|
||||
if (e.data) {
|
||||
try {
|
||||
const errorJson = JSON.parse(e.data);
|
||||
if (errorJson?.error) {
|
||||
errorMessage = errorJson.error.message || errorMessage;
|
||||
errorCode = errorJson.error.code || null;
|
||||
}
|
||||
} catch (_) {
|
||||
// not JSON, use raw data as error message
|
||||
}
|
||||
}
|
||||
|
||||
const errorInfo = handleApiError(new Error(errorMessage));
|
||||
errorInfo.readyState = source.readyState;
|
||||
@@ -393,8 +418,19 @@ export const useApiRequest = (
|
||||
}));
|
||||
setActiveDebugTab(DEBUG_TABS.RESPONSE);
|
||||
|
||||
streamMessageUpdate(errorMessage, 'content');
|
||||
completeMessage(MESSAGE_STATUS.ERROR);
|
||||
setMessage((prevMessage) => {
|
||||
const newMessages = [...prevMessage];
|
||||
const lastMessage = newMessages[newMessages.length - 1];
|
||||
if (lastMessage && lastMessage.status !== MESSAGE_STATUS.COMPLETE && lastMessage.status !== MESSAGE_STATUS.ERROR) {
|
||||
newMessages[newMessages.length - 1] = {
|
||||
...lastMessage,
|
||||
content: (lastMessage.content || '') + errorMessage,
|
||||
errorCode: errorCode,
|
||||
status: MESSAGE_STATUS.ERROR,
|
||||
};
|
||||
}
|
||||
return newMessages;
|
||||
});
|
||||
sseSourceRef.current = null;
|
||||
source.close();
|
||||
}
|
||||
@@ -446,6 +482,7 @@ export const useApiRequest = (
|
||||
[
|
||||
setDebugData,
|
||||
setActiveDebugTab,
|
||||
setMessage,
|
||||
streamMessageUpdate,
|
||||
completeMessage,
|
||||
t,
|
||||
|
||||
Vendored
+36
-133
@@ -250,6 +250,7 @@
|
||||
"price_xxx 的商品价格 ID,新建产品后可获得": "Product price ID for price_xxx, available after creating new product",
|
||||
"safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "The safety_identifier field helps OpenAI identify application users who may violate usage policies. Disabled by default to protect user privacy",
|
||||
"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",
|
||||
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "The speed field controls Claude inference speed mode. Disabled by default to avoid unintentionally switching to fast mode",
|
||||
"sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Stripe key for sk_xxx or rk_xxx, sensitive information not displayed",
|
||||
"standard 已被移除,vip 用户看不到": "standard has been removed, vip users cannot see it",
|
||||
"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",
|
||||
@@ -410,7 +411,7 @@
|
||||
"以下上游数据可能不可信:": "The following upstream data may not be reliable: ",
|
||||
"以下文件解析失败,已忽略:{{list}}": "The following files failed to parse and have been ignored: {{list}}",
|
||||
"以及": "and",
|
||||
"仪表盘设置": "Dashboard Settings",
|
||||
"仪表盘设置": "Dashboard",
|
||||
"价格": "Pricing",
|
||||
"价格摘要": "Price Summary",
|
||||
"价格暂时不可用,请稍后重试": "Price temporarily unavailable, please try again later",
|
||||
@@ -440,11 +441,11 @@
|
||||
"余额充值管理": "Balance recharge management",
|
||||
"作废": "Invalidate",
|
||||
"作废于": "Invalidated at",
|
||||
"下一次重置": "Next reset",
|
||||
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "After invalidation, the subscription becomes invalid immediately. History is not affected. Continue?",
|
||||
"作用域": "Scope",
|
||||
"作用域:包含分组": "Scope: Include Group",
|
||||
"作用域:包含模型名称": "Scope: Include Model Name",
|
||||
"开启后,模型名称会参与 cache key(不同模型隔离)。": "When enabled, the model name is included in the cache key (isolates different models).",
|
||||
"作用域:包含规则名称": "Scope: Include Rule Name",
|
||||
"你似乎并没有修改什么": "You seem to have not modified anything",
|
||||
"你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "You can manually add them under “Custom model names”, click Fill and submit, or use the actions below to handle them automatically.",
|
||||
@@ -581,6 +582,7 @@
|
||||
"允许 inference_geo 透传": "Allow inference_geo Pass-through",
|
||||
"允许 safety_identifier 透传": "Allow safety_identifier Pass-through",
|
||||
"允许 service_tier 透传": "Allow service_tier Pass-through",
|
||||
"允许 speed 透传": "Allow speed Pass-through",
|
||||
"允许 stream_options.include_obfuscation 透传": "Allow stream_options.include_obfuscation Pass-through",
|
||||
"允许不安全的 Origin(HTTP)": "Allow insecure Origin (HTTP)",
|
||||
"允许回调(会泄露服务器 IP 地址)": "Allow callback (will leak server IP address)",
|
||||
@@ -679,7 +681,7 @@
|
||||
"其他": "Other",
|
||||
"其他注册选项": "Other registration options",
|
||||
"其他登录选项": "Other login options",
|
||||
"其他设置": "Other Settings",
|
||||
"其他设置": "Other",
|
||||
"其他详情": "Other details",
|
||||
"内存 阈值 (%)": "Memory Threshold (%)",
|
||||
"内存使用率超过此值时拒绝请求": "Reject requests when memory usage exceeds this value",
|
||||
@@ -700,7 +702,7 @@
|
||||
"分类名称": "Category Name",
|
||||
"分组": "Group",
|
||||
"分组JSON设置": "Group JSON Settings",
|
||||
"分组与模型定价设置": "Group and Model Pricing Settings",
|
||||
"分组与模型定价设置": "Group & Model Pricing",
|
||||
"分组价格": "Group price",
|
||||
"分组倍率": "Group ratio",
|
||||
"分组倍率设置": "Group ratio settings",
|
||||
@@ -774,6 +776,7 @@
|
||||
"刷新统计": "Refresh Stats",
|
||||
"刷新缓存统计": "Refresh Cache Statistics",
|
||||
"刷新缓存统计失败": "Failed to refresh cache statistics",
|
||||
"刷新页面": "Reload Page",
|
||||
"前往 io.net API Keys": "Go to io.net API Keys",
|
||||
"前往设置": "Go to Settings",
|
||||
"前往设置页面": "Go to Settings Page",
|
||||
@@ -825,6 +828,8 @@
|
||||
"原密码": "Original Password",
|
||||
"原生格式": "Native format",
|
||||
"原生额度": "Raw quota",
|
||||
"使用原生额度输入": "Use raw quota input",
|
||||
"收起原生额度输入": "Hide raw quota input",
|
||||
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Deduplication completed: {{before}} keys before deduplication, {{after}} keys after deduplication",
|
||||
"参与官方同步": "Participate in official sync",
|
||||
"参数": "parameter",
|
||||
@@ -925,6 +930,7 @@
|
||||
"启用Gemini思考后缀适配": "Enable Gemini thinking suffix adaptation",
|
||||
"启用Ping间隔": "Enable Ping interval",
|
||||
"启用SMTP SSL": "Enable SMTP SSL",
|
||||
"强制使用 AUTH LOGIN": "Force AUTH LOGIN",
|
||||
"启用SSRF防护(推荐开启以保护服务器安全)": "Enable SSRF Protection (Recommended for server security)",
|
||||
"启用供应商": "Enable Provider",
|
||||
"启用全部": "Enable all",
|
||||
@@ -1362,6 +1368,7 @@
|
||||
"开启后,将定期发送ping数据保持连接活跃": "After enabling, ping data will be sent periodically to keep the connection active",
|
||||
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "After enabling, when the current group channel fails, it will try the next group's channel in order",
|
||||
"开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启": "When enabled, all requests will be directly forwarded to the upstream without any processing (redirects and channel adaptation will also be disabled). Please enable with caution.",
|
||||
"开启后,模型名称会参与 cache key(不同模型隔离)。": "When enabled, the model name is included in the cache key (isolates different models).",
|
||||
"开启后,若该规则命中且请求失败,将不会切换渠道重试。": "When enabled, if this rule matches and the request fails, no channel switch retry will occur.",
|
||||
"开启后,规则名称会参与 cache key(不同规则隔离)。": "When enabled, the rule name will be part of the cache key (isolated by rule).",
|
||||
"开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)": "When enabled, requests to Claude through this channel will force append ?beta=true (no need for clients to pass this parameter manually)",
|
||||
@@ -1438,7 +1445,7 @@
|
||||
"思考预算占比": "Thinking budget ratio",
|
||||
"性能指标": "Performance Indicators",
|
||||
"性能监控": "Performance Monitor",
|
||||
"性能设置": "Performance Settings",
|
||||
"性能设置": "Performance",
|
||||
"总 GPU 小时": "Total GPU Hours",
|
||||
"总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}": "Total price: text price {{textPrice}} + audio price {{audioPrice}} = {{symbol}}{{total}}",
|
||||
"总分配内存": "Total Allocated Memory",
|
||||
@@ -1592,7 +1599,7 @@
|
||||
"支付方式名称": "Pay Method Name",
|
||||
"支付方式类型": "Pay Method Type",
|
||||
"支付渠道": "Payment Channels",
|
||||
"支付设置": "Payment Settings",
|
||||
"支付设置": "Payment",
|
||||
"支付请求失败": "Payment request failed",
|
||||
"支付金额": "Payment Amount",
|
||||
"支持 Ctrl+V 粘贴图片": "Supports Ctrl+V to paste images",
|
||||
@@ -1900,6 +1907,7 @@
|
||||
"条件规则": "Condition Rules",
|
||||
"条件项设置": "Condition Item Settings",
|
||||
"条日志已清理!": "logs have been cleared!",
|
||||
"条规则": "rules",
|
||||
"条,共": "of",
|
||||
"来源": "Source",
|
||||
"来源于 IO.NET 部署": "From IO.NET Deployment",
|
||||
@@ -1987,6 +1995,7 @@
|
||||
"模型定价,需要登录访问": "Model pricing, requires login to access",
|
||||
"模型广场": "Model Marketplace",
|
||||
"模型拉取失败: {{error}}": "Failed to pull model: {{error}}",
|
||||
"模型排行": "Model ranking",
|
||||
"模型支持的接口端点信息": "Model supported API endpoint information",
|
||||
"模型数据分析": "Model Data Analysis",
|
||||
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
|
||||
@@ -1999,7 +2008,7 @@
|
||||
"模型消耗趋势": "Model consumption trend",
|
||||
"模型版本": "Model version",
|
||||
"模型的详细描述和基本特性": "Detailed description and basic characteristics of the model",
|
||||
"模型相关设置": "Model related settings",
|
||||
"模型相关设置": "Model Related",
|
||||
"模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "The model community needs everyone's contribution. If you find incorrect data or want to contribute new models, please visit:",
|
||||
"模型管理": "Model Management",
|
||||
"模型组": "Model group",
|
||||
@@ -2012,7 +2021,7 @@
|
||||
"模型部署": "Model Deployment",
|
||||
"模型部署服务未启用": "Model deployment service is not enabled",
|
||||
"模型部署管理": "Model Deployment Management",
|
||||
"模型部署设置": "Model Deployment Settings",
|
||||
"模型部署设置": "Model Deployment",
|
||||
"模型配置": "Model Configuration",
|
||||
"模型重定向": "Model mapping",
|
||||
"模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "The following models from the redirect have not been added to the “Models” list and requests will fail due to no available model:",
|
||||
@@ -2145,6 +2154,7 @@
|
||||
"添加公告": "Add Notice",
|
||||
"添加分类": "Add Category",
|
||||
"添加分组": "Add Group",
|
||||
"添加分组规则": "Add Group Rules",
|
||||
"添加后提交": "Submit after adding",
|
||||
"添加启动参数": "Add Startup Args",
|
||||
"添加启动命令": "Add Startup Command",
|
||||
@@ -2161,6 +2171,14 @@
|
||||
"添加键值对": "Add key-value pair",
|
||||
"添加问答": "Add FAQ",
|
||||
"添加额度": "Add quota",
|
||||
"减少": "Subtract",
|
||||
"覆盖": "Override",
|
||||
"调整额度": "Adjust Quota",
|
||||
"调整额度成功": "Quota adjusted successfully",
|
||||
"当前额度": "Current quota",
|
||||
"变更": "Change",
|
||||
"预计结果": "Estimated result",
|
||||
"正数为增加,负数为减少": "Positive to add, negative to subtract",
|
||||
"清理不活跃缓存": "Clean up inactive cache",
|
||||
"清理失败": "Cleanup failed",
|
||||
"清理方式": "Cleanup Mode",
|
||||
@@ -2279,6 +2297,8 @@
|
||||
"用户每周期最多请求完成次数": "User max successful request times per period",
|
||||
"用户每周期最多请求次数": "User max request times per period",
|
||||
"用户注册时看到的网站名称,比如'我的网站'": "Website name users see during registration, e.g. 'My Website'",
|
||||
"用户消耗排行": "User consumption ranking",
|
||||
"用户消耗趋势": "User consumption trend",
|
||||
"用户的基本账户信息": "User basic account information",
|
||||
"用户管理": "User Management",
|
||||
"用户组": "User group",
|
||||
@@ -2369,6 +2389,7 @@
|
||||
"确认冲突项修改": "Confirm conflict item modification",
|
||||
"确认删除": "Confirm deletion",
|
||||
"确认删除模型": "Confirm Delete Model",
|
||||
"确认删除该分组的所有规则?": "Delete all rules for this group?",
|
||||
"确认删除该分组?": "Confirm delete this group?",
|
||||
"确认删除该规则?": "Confirm delete this rule?",
|
||||
"确认取消密码登录": "Confirm cancel password login",
|
||||
@@ -2523,7 +2544,7 @@
|
||||
"系统文档和帮助信息": "System documentation and help information",
|
||||
"系统消息": "System message",
|
||||
"系统管理功能": "System management functions",
|
||||
"系统设置": "System Settings",
|
||||
"系统设置": "System",
|
||||
"系统访问令牌": "System Access Token",
|
||||
"索引": "Index",
|
||||
"紧凑列表": "Compact list",
|
||||
@@ -2552,7 +2573,7 @@
|
||||
"绘图": "Drawing",
|
||||
"绘图任务记录": "Drawing task records",
|
||||
"绘图日志": "Drawing Logs",
|
||||
"绘图设置": "Drawing settings",
|
||||
"绘图设置": "Drawing",
|
||||
"统一的": "The Unified",
|
||||
"统计Tokens": "Statistical Tokens",
|
||||
"统计已重置": "Statistics reset",
|
||||
@@ -2630,7 +2651,7 @@
|
||||
"聊天区域": "Chat Area",
|
||||
"聊天应用名称": "Chat Application Name",
|
||||
"聊天应用名称已存在,请使用其他名称": "Chat application name already exists, please use another name",
|
||||
"聊天设置": "Chat settings",
|
||||
"聊天设置": "Chat",
|
||||
"聊天配置": "Chat configuration",
|
||||
"聊天链接配置错误,请联系管理员": "Chat link configuration error, please contact administrator",
|
||||
"联系我们": "Contact Us",
|
||||
@@ -2880,6 +2901,7 @@
|
||||
"请求参数无效": "Invalid request parameters",
|
||||
"请求发生错误": "An error occurred with the request",
|
||||
"请求发生错误: ": "An error occurred with the request: ",
|
||||
"模型价格未配置": "Model Price Not Configured",
|
||||
"请求后端接口失败:": "Failed to request the backend interface: ",
|
||||
"请求失败": "Request failed",
|
||||
"请求头覆盖": "Request header override",
|
||||
@@ -3067,9 +3089,6 @@
|
||||
"调用次数分布": "Models call distribution",
|
||||
"调用次数排行": "Models call ranking",
|
||||
"调用趋势": "Call trend",
|
||||
"模型排行": "Model ranking",
|
||||
"用户消耗排行": "User consumption ranking",
|
||||
"用户消耗趋势": "User consumption trend",
|
||||
"调试信息": "Debug information",
|
||||
"谨慎": "Cautious",
|
||||
"豆包": "Doubao",
|
||||
@@ -3165,7 +3184,7 @@
|
||||
"过期时间不能早于当前时间!": "Expiration time cannot be earlier than the current time!",
|
||||
"过期时间快捷设置": "Expiration time quick settings",
|
||||
"过期时间格式错误!": "Expiration time format error!",
|
||||
"运营设置": "Operation Settings",
|
||||
"运营设置": "Operation",
|
||||
"运行中": "Running",
|
||||
"运行命令 (Command)": "Command",
|
||||
"运行时长": "Runtime Duration",
|
||||
@@ -3253,7 +3272,7 @@
|
||||
"通道 ${name} 余额更新成功!": "Channel ${name} quota updated successfully!",
|
||||
"通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test successful, model ${model} took ${time.toFixed(2)} seconds.",
|
||||
"通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test successful, took ${time.toFixed(2)} seconds.",
|
||||
"速率限制设置": "Rate limit settings",
|
||||
"速率限制设置": "Rate Limit",
|
||||
"逻辑": "Logic",
|
||||
"邀请": "Invitations",
|
||||
"邀请人": "Inviter",
|
||||
@@ -3417,6 +3436,7 @@
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Audio output: {{tokens}} / 1M * model ratio {{modelRatio}} * audio ratio {{audioRatio}} * audio completion ratio {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"页脚": "Footer",
|
||||
"页面未找到,请检查您的浏览器地址是否正确": "Page not found, please check if your browser address is correct",
|
||||
"页面渲染出错,请刷新页面重试": "An error occurred while rendering the page. Please refresh and try again.",
|
||||
"顶栏管理": "Header Management",
|
||||
"项": "items",
|
||||
"项目": "Project",
|
||||
@@ -3492,123 +3512,6 @@
|
||||
"默认测试模型": "Default Test Model",
|
||||
"默认用户消息": "Default User Message",
|
||||
"默认补全倍率": "Default completion ratio",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Notice: Endpoint mapping is for Model Marketplace display only and does not affect real model invocation. To configure real invocation, please go to Channel Management.",
|
||||
"购买订阅获得模型额度/次数": "Purchase a subscription to get model quota/usage",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "Production RSA private key Base64 (PKCS#8 DER)",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Sandbox RSA private key Base64 (PKCS#8 DER)",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "Production Waffo public key Base64 (X.509 DER)",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Sandbox Waffo public key Base64 (X.509 DER)",
|
||||
"支付方式类型": "Pay Method Type",
|
||||
"支付方式名称": "Pay Method Name",
|
||||
"获取充值配置失败": "Failed to get topup configuration",
|
||||
"获取充值配置异常": "Topup configuration error",
|
||||
"分组相关设置": "Group Related Settings",
|
||||
"保存分组相关设置": "Save Group Related Settings",
|
||||
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "This page only shows models without base pricing. After saving, configured models will be removed from this list automatically.",
|
||||
"没有未设置定价的模型": "No unpriced models",
|
||||
"当前没有未设置定价的模型": "There are currently no models without pricing",
|
||||
"模型计费编辑器": "Model Pricing Editor",
|
||||
"价格摘要": "Price Summary",
|
||||
"当前提示": "Current Notes",
|
||||
"这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。": "This editor uses prices by default and converts them back into the ratio JSON required by the backend when saved.",
|
||||
"当前未启用,需要时再打开即可。": "This field is currently disabled. Enable it when needed.",
|
||||
"下面展示这个模型保存后会写入哪些后端字段,便于和原始 JSON 编辑框保持一致。": "The fields below show which backend values will be written after saving, so you can keep them aligned with the raw JSON editors.",
|
||||
"补全价格已锁定": "Completion price is locked",
|
||||
"后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "Backend fixed ratio: {{ratio}}. This field only displays the converted price.",
|
||||
"这些价格都是可选项,不填也可以。": "All of these prices are optional and can be left empty.",
|
||||
"请先开启并填写音频输入价格。": "Enable and fill in the audio input price first.",
|
||||
"输入模型名称,例如 gpt-4.1": "Enter a model name, for example gpt-4.1",
|
||||
"当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。": "This model currently has both per-request pricing and ratio-based pricing. Saving will overwrite them according to the current billing mode.",
|
||||
"当前模型存在未显式设置输入倍率的扩展倍率;填写输入价格后会自动换算为价格字段。": "This model has derived ratios without an explicit input ratio. Once you fill in the input price, they will be converted into price fields automatically.",
|
||||
"按量计费下需要先填写输入价格,才能保存其它价格项。": "For per-token billing, fill in the input price before saving other price fields.",
|
||||
"填写音频补全价格前,需要先填写音频输入价格。": "Fill in the audio input price before setting the audio completion price.",
|
||||
"模型 {{name}} 缺少输入价格,无法计算补全/缓存/图片/音频价格对应的倍率": "Model {{name}} is missing an input price, so the ratios for completion, cache, image, and audio pricing cannot be calculated.",
|
||||
"模型 {{name}} 缺少音频输入价格,无法计算音频补全倍率": "Model {{name}} is missing an audio input price, so the audio completion ratio cannot be calculated.",
|
||||
"批量应用当前模型价格": "Batch Apply Current Model Pricing",
|
||||
"请先选择一个作为模板的模型": "Please select a model to use as the template first",
|
||||
"请先勾选需要批量设置的模型": "Please select the models you want to update in batch first",
|
||||
"已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型": "Applied the pricing configuration of model {{name}} to {{count}} models in batch",
|
||||
"将把当前编辑中的模型 {{name}} 的价格配置,批量应用到已勾选的 {{count}} 个模型。": "The pricing configuration of the currently edited model {{name}} will be applied to the {{count}} selected models.",
|
||||
"适合同系列模型一起定价,例如把 gpt-5.1 的价格批量同步到 gpt-5.1-high、gpt-5.1-low 等模型。": "Useful for pricing model variants together, for example syncing the pricing of gpt-5.1 to gpt-5.1-high, gpt-5.1-low, and similar models.",
|
||||
"已勾选": "Selected",
|
||||
"当前编辑": "Editing",
|
||||
"已勾选 {{count}} 个模型": "{{count}} models selected",
|
||||
"计费方式": "Billing Mode",
|
||||
"未设置价格": "Price not set",
|
||||
"保存预览": "Save Preview",
|
||||
"基础价格": "Base Pricing",
|
||||
"扩展价格": "Additional Pricing",
|
||||
"额外价格项": "Additional price items",
|
||||
"补全价格": "Completion Price",
|
||||
"缓存读取价格": "Input Cache Read Price",
|
||||
"缓存创建价格": "Input Cache Creation Price",
|
||||
"图片输入价格": "Image Input Price",
|
||||
"音频输入价格": "Audio Input Price",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "Audio input price: {{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格": "Audio Completion Price",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "Audio completion price: {{symbol}}{{price}} / 1M tokens",
|
||||
"适合 MJ / 任务类等按次收费模型。": "Suitable for MJ and other task-based models billed per request.",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "This model's completion ratio is fixed to {{ratio}} by the backend. The completion price cannot be changed here.",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Web search called {{webSearchCallCount}} times",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "File search called {{fileSearchCallCount}} times",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Actual charge: {{symbol}}{{total}} (group pricing adjustment included)",
|
||||
"图片倍率 {{imageRatio}}": "Image ratio {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "Audio ratio {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Standard input: {{tokens}} / 1M * model ratio {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Cached input: {{tokens}} / 1M * model ratio {{modelRatio}} * cache ratio {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Image input: {{tokens}} / 1M * model ratio {{modelRatio}} * image ratio {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Audio input: {{tokens}} / 1M * model ratio {{modelRatio}} * audio ratio {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Output: {{tokens}} / 1M * model ratio {{modelRatio}} * completion ratio {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web search: {{count}} / 1K * unit price {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "File search: {{count}} / 1K * unit price {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Image generation: 1 call * unit price {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "Total: {{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "Model ratio {{modelRatio}}, completion ratio {{completionRatio}}, audio ratio {{audioRatio}}, audio completion ratio {{audioCompletionRatio}}, {{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Text output: {{tokens}} / 1M * model ratio {{modelRatio}} * completion ratio {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Audio output: {{tokens}} / 1M * model ratio {{modelRatio}} * audio ratio {{audioRatio}} * audio completion ratio {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "Total: text {{textTotal}} + audio {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "Model ratio {{modelRatio}}, output ratio {{completionRatio}}, cache ratio {{cacheRatio}}, {{ratioType}} {{ratio}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Cache read: {{tokens}} / 1M * model ratio {{modelRatio}} * cache ratio {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Cache creation: {{tokens}} / 1M * model ratio {{modelRatio}} * cache creation ratio {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "5m cache creation: {{tokens}} / 1M * model ratio {{modelRatio}} * 5m cache creation ratio {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h cache creation: {{tokens}} / 1M * model ratio {{modelRatio}} * 1h cache creation ratio {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Output: {{tokens}} / 1M * model ratio {{modelRatio}} * output ratio {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "Empty",
|
||||
"{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x",
|
||||
"模型价格:{{symbol}}{{price}}": "Model price: {{symbol}}{{price}}",
|
||||
"模型价格 {{price}}": "Model price {{price}}",
|
||||
"缓存读 {{price}} / 1M tokens": "Cache read {{price}} / 1M tokens",
|
||||
"5m缓存创建 {{price}} / 1M tokens": "5m cache creation {{price}} / 1M tokens",
|
||||
"1h缓存创建 {{price}} / 1M tokens": "1h cache creation {{price}} / 1M tokens",
|
||||
"缓存创建 {{price}} / 1M tokens": "Cache creation {{price}} / 1M tokens",
|
||||
"图片输入 {{price}} / 1M tokens": "Image input {{price}} / 1M tokens",
|
||||
"输入 {{price}} / 1M tokens": "Input {{price}} / 1M tokens",
|
||||
"缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Cache creation {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "5m cache creation {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "1h cache creation {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}": "(Input {{nonImageInput}} tokens + Image input {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"图片输入价格:{{symbol}}{{total}} / 1M tokens": "Image input price: {{symbol}}{{total}} / 1M tokens",
|
||||
"文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Text prompt {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + Text completion {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + Audio prompt {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + Audio completion {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"缓存读取价格:{{symbol}}{{total}} / 1M tokens": "Cache read price: {{symbol}}{{total}} / 1M tokens",
|
||||
"补全 {{completion}} tokens * 输出倍率 {{completionRatio}}": "Completion {{completion}} tokens * Output ratio {{completionRatio}}",
|
||||
"补全倍率 {{completionRatio}}": "Completion ratio {{completionRatio}}",
|
||||
"输入价格:{{symbol}}{{price}} / 1M tokens": "Input Price: {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格 {{symbol}}{{price}} / 1M tokens": "Output Price {{symbol}}{{price}} / 1M tokens",
|
||||
"输出价格:{{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.",
|
||||
"复制密钥": "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",
|
||||
"页面渲染出错,请刷新页面重试": "An error occurred while rendering the page. Please refresh and try again.",
|
||||
"刷新页面": "Reload Page",
|
||||
"(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Currently only supports Epay interface, the default callback address is the server address above!)",
|
||||
",当前无生效订阅,将自动使用钱包": ", no active subscription. Wallet will be used automatically.",
|
||||
",时间:": ",time:",
|
||||
|
||||
Vendored
+28
-142
@@ -246,6 +246,7 @@
|
||||
"price_xxx 的商品价格 ID,新建产品后可获得": "ID de prix du produit price_xxx, peut être obtenu après la création d'un nouveau produit",
|
||||
"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",
|
||||
"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",
|
||||
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Le champ speed contrôle le mode de vitesse d'inférence de Claude. Désactivé par défaut pour éviter un passage involontaire au mode fast",
|
||||
"sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Clé secrète Stripe sk_xxx ou rk_xxx, les informations sensibles ne sont pas affichées",
|
||||
"standard 已被移除,vip 用户看不到": "standard has been removed, vip users cannot see it",
|
||||
"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",
|
||||
@@ -435,11 +436,11 @@
|
||||
"余额充值管理": "Recharge du solde",
|
||||
"作废": "Invalider",
|
||||
"作废于": "Invalidé le",
|
||||
"下一次重置": "Prochaine réinitialisation",
|
||||
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "Après invalidation, l'abonnement devient immédiatement invalide. L'historique n'est pas affecté. Continuer ?",
|
||||
"作用域": "Portée",
|
||||
"作用域:包含分组": "Portée : inclure le groupe",
|
||||
"作用域:包含模型名称": "Portée : inclure le nom du modèle",
|
||||
"开启后,模型名称会参与 cache key(不同模型隔离)。": "Lorsque activé, le nom du modèle est inclus dans la clé de cache (isole les différents modèles).",
|
||||
"作用域:包含规则名称": "Portée : inclure le nom de la règle",
|
||||
"你似乎并没有修改什么": "Vous ne semblez rien avoir modifié",
|
||||
"你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "Vous pouvez les ajouter manuellement dans « Noms de modèles personnalisés », cliquer sur Remplir puis soumettre, ou utiliser directement les actions ci-dessous pour les traiter automatiquement.",
|
||||
@@ -574,6 +575,7 @@
|
||||
"允许 inference_geo 透传": "Autoriser la transmission de inference_geo",
|
||||
"允许 safety_identifier 透传": "Autoriser le passage de safety_identifier",
|
||||
"允许 service_tier 透传": "Autoriser le passage de service_tier",
|
||||
"允许 speed 透传": "Autoriser la transmission de speed",
|
||||
"允许 stream_options.include_obfuscation 透传": "Autoriser la transmission de stream_options.include_obfuscation",
|
||||
"允许不安全的 Origin(HTTP)": "Autoriser une origine non sécurisée (HTTP)",
|
||||
"允许回调(会泄露服务器 IP 地址)": "Autoriser le rappel (divulguera l'adresse IP du serveur)",
|
||||
@@ -696,7 +698,7 @@
|
||||
"分类名称": "Nom de la catégorie",
|
||||
"分组": "Groupe",
|
||||
"分组JSON设置": "Group JSON Settings",
|
||||
"分组与模型定价设置": "Groupe et tarification",
|
||||
"分组与模型定价设置": "Groupes & tarification des modèles",
|
||||
"分组价格": "Prix de groupe",
|
||||
"分组倍率": "Ratio",
|
||||
"分组倍率设置": "Ratio de groupe",
|
||||
@@ -770,6 +772,7 @@
|
||||
"刷新统计": "Actualiser les statistiques",
|
||||
"刷新缓存统计": "Actualiser les statistiques du cache",
|
||||
"刷新缓存统计失败": "Échec de l'actualisation des statistiques du cache",
|
||||
"刷新页面": "Recharger la page",
|
||||
"前往 io.net API Keys": "Go to io.net API Keys",
|
||||
"前往设置": "Go to Settings",
|
||||
"前往设置页面": "Go to Settings Page",
|
||||
@@ -821,6 +824,8 @@
|
||||
"原密码": "Mot de passe original",
|
||||
"原生格式": "Format natif",
|
||||
"原生额度": "Quota brut",
|
||||
"使用原生额度输入": "Saisir le quota brut",
|
||||
"收起原生额度输入": "Masquer la saisie du quota brut",
|
||||
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Doublons supprimés : {{before}} clés avant, {{after}} clés après",
|
||||
"参与官方同步": "Participer à la synchronisation officielle",
|
||||
"参数": "paramètre",
|
||||
@@ -920,6 +925,7 @@
|
||||
"启用Gemini思考后缀适配": "Activer l'adaptation du suffixe de la pensée Gemini",
|
||||
"启用Ping间隔": "Activer l'intervalle de ping",
|
||||
"启用SMTP SSL": "Activer SMTP SSL",
|
||||
"强制使用 AUTH LOGIN": "Forcer AUTH LOGIN",
|
||||
"启用SSRF防护(推荐开启以保护服务器安全)": "Activer la protection SSRF (recommandé pour la sécurité du serveur)",
|
||||
"启用供应商": "Activer le fournisseur",
|
||||
"启用全部": "Activer tout",
|
||||
@@ -1361,6 +1367,7 @@
|
||||
"开启后,将定期发送ping数据保持连接活跃": "Après activation, des données ping seront envoyées périodiquement pour maintenir la connexion active",
|
||||
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "Après activation, lorsque le canal du groupe actuel échoue, il essaiera le canal du groupe suivant dans l'ordre",
|
||||
"开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启": "Après activation, toutes les requêtes seront directement transmises en amont sans aucun traitement (la redirection et l'adaptation de canal seront également désactivées), veuillez activer avec prudence",
|
||||
"开启后,模型名称会参与 cache key(不同模型隔离)。": "Lorsque activé, le nom du modèle est inclus dans la clé de cache (isole les différents modèles).",
|
||||
"开启后,若该规则命中且请求失败,将不会切换渠道重试。": "Une fois activé, si cette règle est déclenchée et que la requête échoue, aucune nouvelle tentative sur un autre canal ne sera effectuée.",
|
||||
"开启后,规则名称会参与 cache key(不同规则隔离)。": "Une fois activé, le nom de la règle fera partie de la clé de cache (isolation par règle).",
|
||||
"开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)": "Une fois activé, les requêtes à Claude via ce canal ajouteront automatiquement ?beta=true (pas besoin de le passer manuellement côté client)",
|
||||
@@ -1435,7 +1442,7 @@
|
||||
"思考预算占比": "Ratio du budget de la pensée",
|
||||
"性能指标": "Indicateurs de performance",
|
||||
"性能监控": "Surveillance des performances",
|
||||
"性能设置": "Paramètres de performance",
|
||||
"性能设置": "Performance",
|
||||
"总 GPU 小时": "Total GPU Hours",
|
||||
"总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}": "Prix total : prix du texte {{textPrice}} + prix de l'audio {{audioPrice}} = {{symbol}}{{total}}",
|
||||
"总分配内存": "Mémoire totale allouée",
|
||||
@@ -1884,6 +1891,7 @@
|
||||
"条件规则": "Règles de condition",
|
||||
"条件项设置": "Paramètres des éléments de condition",
|
||||
"条日志已清理!": "les journaux ont été effacés !",
|
||||
"条规则": "rules",
|
||||
"条,共": "sur",
|
||||
"来源": "Source",
|
||||
"来源于 IO.NET 部署": "From IO.NET Deployment",
|
||||
@@ -1969,6 +1977,7 @@
|
||||
"模型定价,需要登录访问": "Tarification du modèle, nécessite une connexion pour y accéder",
|
||||
"模型广场": "Marché des modèles",
|
||||
"模型拉取失败: {{error}}": "Failed to pull model: {{error}}",
|
||||
"模型排行": "Classement des modèles",
|
||||
"模型支持的接口端点信息": "Informations sur les points de terminaison de l'API pris en charge par le modèle",
|
||||
"模型数据分析": "Analyse des données du modèle",
|
||||
"模型映射必须是合法的 JSON 格式!": "Le mappage de modèles doit être au format JSON valide !",
|
||||
@@ -1981,7 +1990,7 @@
|
||||
"模型消耗趋势": "Tendance de la consommation des modèles",
|
||||
"模型版本": "Version du modèle",
|
||||
"模型的详细描述和基本特性": "Description détaillée et caractéristiques de base du modèle",
|
||||
"模型相关设置": "Paramètres liés au modèle",
|
||||
"模型相关设置": "Modèle associé",
|
||||
"模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "La communauté des modèles a besoin de la contribution de tous. Si vous trouvez des données incorrectes ou si vous souhaitez contribuer à de nouvelles données de modèle, veuillez visiter :",
|
||||
"模型管理": "Modèles",
|
||||
"模型组": "Groupe de modèles",
|
||||
@@ -1994,7 +2003,7 @@
|
||||
"模型部署": "Model Deployment",
|
||||
"模型部署服务未启用": "Model deployment service is not enabled",
|
||||
"模型部署管理": "Model Deployment Management",
|
||||
"模型部署设置": "Model Deployment Settings",
|
||||
"模型部署设置": "Déploiement de modèles",
|
||||
"模型配置": "Configuration du modèle",
|
||||
"模型重定向": "Redirection de modèle",
|
||||
"模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "Les modèles suivants provenant de la redirection n'ont pas été ajoutés à la liste « Modèles », l'appel échouera faute de modèle disponible :",
|
||||
@@ -2124,6 +2133,7 @@
|
||||
"添加公告": "Ajouter un avis",
|
||||
"添加分类": "Ajouter une catégorie",
|
||||
"添加分组": "Add Group",
|
||||
"添加分组规则": "Add Group Rules",
|
||||
"添加后提交": "Soumettre après ajout",
|
||||
"添加启动参数": "Add Startup Args",
|
||||
"添加启动命令": "Add Startup Command",
|
||||
@@ -2139,6 +2149,14 @@
|
||||
"添加键值对": "Ajouter une paire clé-valeur",
|
||||
"添加问答": "Ajouter une FAQ",
|
||||
"添加额度": "Ajouter un quota",
|
||||
"减少": "Soustraire",
|
||||
"覆盖": "Remplacer",
|
||||
"调整额度": "Ajuster le quota",
|
||||
"调整额度成功": "Quota ajusté avec succès",
|
||||
"当前额度": "Quota actuel",
|
||||
"变更": "Modification",
|
||||
"预计结果": "Résultat estimé",
|
||||
"正数为增加,负数为减少": "Positif pour ajouter, négatif pour soustraire",
|
||||
"清理不活跃缓存": "Nettoyer le cache inactif",
|
||||
"清理失败": "Échec du nettoyage",
|
||||
"清理方式": "Mode de nettoyage",
|
||||
@@ -2254,6 +2272,8 @@
|
||||
"用户每周期最多请求完成次数": "Nombre maximal de requêtes utilisateur réussies par période",
|
||||
"用户每周期最多请求次数": "Nombre maximal de requêtes utilisateur par période",
|
||||
"用户注册时看到的网站名称,比如'我的网站'": "Nom du site Web que les utilisateurs voient lors de l'inscription, par exemple 'Mon site Web'",
|
||||
"用户消耗排行": "Classement de consommation des utilisateurs",
|
||||
"用户消耗趋势": "Tendance de consommation des utilisateurs",
|
||||
"用户的基本账户信息": "Informations de base du compte utilisateur",
|
||||
"用户管理": "Utilisateurs",
|
||||
"用户组": "Groupe d'utilisateurs",
|
||||
@@ -2345,6 +2365,7 @@
|
||||
"确认冲突项修改": "Confirmer la modification de l'élément de conflit",
|
||||
"确认删除": "Confirmer la suppression",
|
||||
"确认删除模型": "Confirm Delete Model",
|
||||
"确认删除该分组的所有规则?": "Delete all rules for this group?",
|
||||
"确认删除该分组?": "Confirm delete this group?",
|
||||
"确认删除该规则?": "Confirm delete this rule?",
|
||||
"确认取消密码登录": "Confirmer l'annulation de la connexion par mot de passe",
|
||||
@@ -2853,6 +2874,7 @@
|
||||
"请求参数无效": "Invalid request parameters",
|
||||
"请求发生错误": "Une erreur s'est produite lors de la demande",
|
||||
"请求发生错误: ": "Une erreur s'est produite lors de la demande : ",
|
||||
"模型价格未配置": "Prix du modèle non configuré",
|
||||
"请求后端接口失败:": "Échec de la requête de l'interface backend : ",
|
||||
"请求失败": "Échec de la demande",
|
||||
"请求头覆盖": "Remplacement des en-têtes de demande",
|
||||
@@ -3040,9 +3062,6 @@
|
||||
"调用次数分布": "Distribution des appels de modèles",
|
||||
"调用次数排行": "Classement des appels de modèles",
|
||||
"调用趋势": "Tendance des appels",
|
||||
"模型排行": "Classement des modèles",
|
||||
"用户消耗排行": "Classement de consommation des utilisateurs",
|
||||
"用户消耗趋势": "Tendance de consommation des utilisateurs",
|
||||
"调试信息": "Informations de débogage",
|
||||
"谨慎": "Prudent",
|
||||
"豆包": "Doubao",
|
||||
@@ -3382,6 +3401,7 @@
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie audio : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio audio {{audioRatio}} * ratio de complétion audio {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"页脚": "Pied de page",
|
||||
"页面未找到,请检查您的浏览器地址是否正确": "Page non trouvée, veuillez vérifier si l'adresse de votre navigateur est correcte",
|
||||
"页面渲染出错,请刷新页面重试": "Une erreur est survenue lors du rendu de la page. Veuillez rafraîchir et réessayer.",
|
||||
"顶栏管理": "En-tête",
|
||||
"项": "éléments",
|
||||
"项目": "Élément",
|
||||
@@ -3448,140 +3468,6 @@
|
||||
"默认测试模型": "Modèle de test par défaut",
|
||||
"默认用户消息": "Bonjour",
|
||||
"默认补全倍率": "Taux de complétion par défaut",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Remarque : la correspondance des endpoints sert uniquement à l'affichage dans la place de marché des modèles et n'affecte pas l'invocation réelle. Pour configurer l'invocation réelle, veuillez aller dans « Gestion des canaux ».",
|
||||
"购买订阅获得模型额度/次数": "Acheter un abonnement pour obtenir des quotas/usages de modèles",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "Clé privée RSA Base64 (PKCS#8 DER) de production",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Clé privée RSA Base64 (PKCS#8 DER) de sandbox",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "Clé publique Waffo Base64 (X.509 DER) de production",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Clé publique Waffo Base64 (X.509 DER) de sandbox",
|
||||
"支付方式类型": "Type de méthode de paiement",
|
||||
"支付方式名称": "Nom de méthode de paiement",
|
||||
"获取充值配置失败": "Échec de la récupération de la configuration de recharge",
|
||||
"获取充值配置异常": "Erreur de configuration de recharge",
|
||||
"分组相关设置": "Paramètres liés aux groupes",
|
||||
"保存分组相关设置": "Enregistrer les paramètres liés aux groupes",
|
||||
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "Cette page n'affiche que les modèles sans prix ou ratio de base. Après enregistrement, ils seront retirés automatiquement de cette liste.",
|
||||
"没有未设置定价的模型": "Aucun modèle sans prix",
|
||||
"当前没有未设置定价的模型": "Il n'y a actuellement aucun modèle sans prix",
|
||||
"模型计费编辑器": "Éditeur de tarification des modèles",
|
||||
"价格摘要": "Résumé des prix",
|
||||
"当前提示": "Informations actuelles",
|
||||
"这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。": "Cette interface utilise les prix par défaut et les reconvertit automatiquement en JSON de ratios requis par le backend lors de l'enregistrement.",
|
||||
"当前未启用,需要时再打开即可。": "Ce champ est actuellement désactivé. Activez-le si nécessaire.",
|
||||
"下面展示这个模型保存后会写入哪些后端字段,便于和原始 JSON 编辑框保持一致。": "Les champs backend écrits après l'enregistrement sont affichés ci-dessous afin de rester cohérents avec les éditeurs JSON bruts.",
|
||||
"补全价格已锁定": "Le prix de complétion est verrouillé",
|
||||
"后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "Ratio fixé par le backend : {{ratio}}. Ce champ affiche uniquement le prix converti.",
|
||||
"这些价格都是可选项,不填也可以。": "Tous ces prix sont optionnels et peuvent être laissés vides.",
|
||||
"请先开启并填写音频输入价格。": "Activez et renseignez d'abord le prix d'entrée audio.",
|
||||
"输入模型名称,例如 gpt-4.1": "Saisissez un nom de modèle, par exemple gpt-4.1",
|
||||
"当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。": "Ce modèle possède actuellement à la fois une tarification par requête et une configuration par ratio. L'enregistrement écrasera selon le mode de facturation actuel.",
|
||||
"当前模型存在未显式设置输入倍率的扩展倍率;填写输入价格后会自动换算为价格字段。": "Ce modèle contient des ratios étendus sans ratio d'entrée explicite. Après saisie du prix d'entrée, ils seront convertis automatiquement en champs de prix.",
|
||||
"按量计费下需要先填写输入价格,才能保存其它价格项。": "En facturation au volume, il faut d'abord renseigner le prix d'entrée avant d'enregistrer les autres prix.",
|
||||
"填写音频补全价格前,需要先填写音频输入价格。": "Renseignez d'abord le prix d'entrée audio avant de définir le prix de complétion audio.",
|
||||
"模型 {{name}} 缺少输入价格,无法计算补全/缓存/图片/音频价格对应的倍率": "Le modèle {{name}} n'a pas de prix d'entrée, impossible de calculer les ratios correspondants pour la complétion, le cache, les images et l'audio.",
|
||||
"模型 {{name}} 缺少音频输入价格,无法计算音频补全倍率": "Le modèle {{name}} n'a pas de prix d'entrée audio, impossible de calculer le ratio de complétion audio.",
|
||||
"批量应用当前模型价格": "Appliquer en lot le prix du modèle actuel",
|
||||
"请先选择一个作为模板的模型": "Veuillez d'abord choisir un modèle comme modèle de référence",
|
||||
"请先勾选需要批量设置的模型": "Veuillez d'abord sélectionner les modèles à configurer en lot",
|
||||
"已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型": "La configuration tarifaire du modèle {{name}} a été appliquée à {{count}} modèles en lot",
|
||||
"将把当前编辑中的模型 {{name}} 的价格配置,批量应用到已勾选的 {{count}} 个模型。": "La configuration tarifaire du modèle actuellement édité {{name}} sera appliquée aux {{count}} modèles sélectionnés.",
|
||||
"适合同系列模型一起定价,例如把 gpt-5.1 的价格批量同步到 gpt-5.1-high、gpt-5.1-low 等模型。": "Pratique pour tarifer ensemble des variantes d'un même modèle, par exemple synchroniser le prix de gpt-5.1 vers gpt-5.1-high, gpt-5.1-low et autres variantes similaires.",
|
||||
"已勾选": "Sélectionné",
|
||||
"当前编辑": "En cours d'édition",
|
||||
"已勾选 {{count}} 个模型": "{{count}} modèles sélectionnés",
|
||||
"计费方式": "Mode de facturation",
|
||||
"未设置价格": "Prix non défini",
|
||||
"保存预览": "Aperçu avant enregistrement",
|
||||
"基础价格": "Prix de base",
|
||||
"扩展价格": "Prix supplémentaires",
|
||||
"额外价格项": "Éléments de prix supplémentaires",
|
||||
"补全价格": "Prix de complétion",
|
||||
"缓存读取价格": "Prix de lecture du cache d'entrée",
|
||||
"缓存创建价格": "Prix de création du cache d'entrée",
|
||||
"图片输入价格": "Prix d'entrée image",
|
||||
"音频输入价格": "Prix d'entrée audio",
|
||||
"音频补全价格": "Prix de complétion audio",
|
||||
"适合 MJ / 任务类等按次收费模型。": "Convient aux modèles MJ et autres modèles facturés à la requête.",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "Le ratio de complétion de ce modèle est fixé à {{ratio}} par le backend. Le prix de complétion ne peut pas être modifié ici.",
|
||||
"计费显示模式": "Mode d'affichage de la facturation",
|
||||
"价格模式(默认)": "Mode prix (par défaut)",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "Prix du modèle {{symbol}}{{price}} / requête",
|
||||
"按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Par requête {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "Prix du modèle : {{symbol}}{{price}} / requête",
|
||||
"按次:{{symbol}}{{price}}": "Par requête : {{symbol}}{{price}}",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Montant facturé réel : {{symbol}}{{total}} (ajustement tarifaire de groupe inclus)",
|
||||
"缓存读取价格:{{symbol}}{{price}} / 1M tokens": "Prix de lecture du cache : {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取价格 {{symbol}}{{price}} / 1M tokens": "Prix de lecture du cache {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Prix de création du cache : {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Prix de création du cache {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Prix de création du cache 5m : {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Prix de création du cache 5m {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Prix de création du cache 1h : {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Prix de création du cache 1h {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格:{{symbol}}{{price}} / 1M tokens": "Prix d'entrée image : {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格 {{symbol}}{{price}} / 1M tokens": "Prix d'entrée image {{symbol}}{{price}} / 1M tokens",
|
||||
"输入价格 {{symbol}}{{price}} / 1M tokens": "Prix d'entrée {{symbol}}{{price}} / 1M tokens",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "Prix d'entrée audio : {{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "Prix de complétion audio : {{symbol}}{{price}} / 1M tokens",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Recherche Web appelée {{webSearchCallCount}} fois",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "Recherche de fichier appelée {{fileSearchCallCount}} fois",
|
||||
"图片倍率 {{imageRatio}}": "Ratio d'image {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "Ratio audio {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée standard : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée en cache : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio du cache {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée d'image : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio d'image {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Entrée audio : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio audio {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de complétion {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Recherche Web : {{count}} / 1K * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Recherche de fichier : {{count}} / 1K * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Génération d'image : 1 appel * prix unitaire {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "Total : {{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "Ratio du modèle {{modelRatio}}, ratio de complétion {{completionRatio}}, ratio audio {{audioRatio}}, ratio de complétion audio {{audioCompletionRatio}}, {{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie texte : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de complétion {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie audio : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio audio {{audioRatio}} * ratio de complétion audio {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "Total : partie texte {{textTotal}} + partie audio {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "Ratio du modèle {{modelRatio}}, ratio de sortie {{completionRatio}}, ratio du cache {{cacheRatio}}, {{ratioType}} {{ratio}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Lecture du cache : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio du cache {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache 5m : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Création du cache 1h : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de création du cache 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Sortie : {{tokens}} / 1M * ratio du modèle {{modelRatio}} * ratio de sortie {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "Vide",
|
||||
"{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x",
|
||||
"模型价格:{{symbol}}{{price}}": "Prix du modèle : {{symbol}}{{price}}",
|
||||
"模型价格 {{price}}": "Prix du modèle {{price}}",
|
||||
"缓存读 {{price}} / 1M tokens": "Lecture du cache {{price}} / 1M tokens",
|
||||
"5m缓存创建 {{price}} / 1M tokens": "Création de cache 5m {{price}} / 1M tokens",
|
||||
"1h缓存创建 {{price}} / 1M tokens": "Création de cache 1h {{price}} / 1M tokens",
|
||||
"缓存创建 {{price}} / 1M tokens": "Création de cache {{price}} / 1M tokens",
|
||||
"图片输入 {{price}} / 1M tokens": "Entrée image {{price}} / 1M tokens",
|
||||
"输入 {{price}} / 1M tokens": "Entrée {{price}} / 1M tokens",
|
||||
"缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Cache {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Création de cache {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Création de cache 5m {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Création de cache 1h {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}": "(Entrée {{nonImageInput}} tokens + Entrée image {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"图片输入价格:{{symbol}}{{total}} / 1M tokens": "Prix d'entrée image : {{symbol}}{{total}} / 1M tokens",
|
||||
"文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Prompt texte {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + Complétion texte {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + Prompt audio {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + Complétion audio {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Prix du modèle {{symbol}}{{price}} / requête * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"缓存读取价格:{{symbol}}{{total}} / 1M tokens": "Prix de lecture du cache : {{symbol}}{{total}} / 1M tokens",
|
||||
"补全 {{completion}} tokens * 输出倍率 {{completionRatio}}": "Complétion {{completion}} tokens * Ratio de sortie {{completionRatio}}",
|
||||
"补全倍率 {{completionRatio}}": "Ratio de complétion {{completionRatio}}",
|
||||
"输入价格:{{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",
|
||||
"复制密钥": "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",
|
||||
"页面渲染出错,请刷新页面重试": "Une erreur est survenue lors du rendu de la page. Veuillez rafraîchir et réessayer.",
|
||||
"刷新页面": "Recharger la page",
|
||||
"(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Actuellement, seule l'interface Epay est prise en charge, l'adresse du serveur ci-dessus est utilisée par défaut comme adresse de rappel !)",
|
||||
",当前无生效订阅,将自动使用钱包": ", aucun abonnement actif, le portefeuille sera utilisé automatiquement.",
|
||||
",时间:": ", time:",
|
||||
|
||||
Vendored
+37
-151
@@ -242,6 +242,7 @@
|
||||
"price_xxx 的商品价格 ID,新建产品后可获得": "price_xxx の料金ID。新規製品の作成後に取得できます",
|
||||
"safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "safety_identifierフィールドは、OpenAIが利用ポリシーに違反する可能性のあるアプリユーザーを特定するために使用されます。ユーザーのプライバシーを保護するため、デフォルトでは無効です",
|
||||
"service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "service_tierフィールドはサービス階層の指定に使用されます。パススルーを許可すると実際の課金額が想定を上回る場合があるため、追加料金を避けるためにデフォルトでは無効になっています",
|
||||
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "speed フィールドは Claude の推論速度モードを制御します。意図せず fast モードへ切り替わるのを避けるため、デフォルトで無効です",
|
||||
"sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "sk_xxx または rk_xxx のStripe APIキー。機密情報は表示されません",
|
||||
"standard 已被移除,vip 用户看不到": "standard は削除され、vipユーザーには表示されません",
|
||||
"store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "storeフィールドは、製品の評価と最適化のためにOpenAIがリクエストデータを保存することを許可します。デフォルトでは無効です。有効にすると、Codexが正常に利用できなくなる場合があります",
|
||||
@@ -401,7 +402,7 @@
|
||||
"以下上游数据可能不可信:": "以下のアップストリームデータは信頼できない可能性があります:",
|
||||
"以下文件解析失败,已忽略:{{list}}": "以下のファイルは解析に失敗したため無視されました:{{list}}",
|
||||
"以及": "および",
|
||||
"仪表盘设置": "ダッシュボード設定",
|
||||
"仪表盘设置": "ダッシュボード",
|
||||
"价格": "料金",
|
||||
"价格摘要": "価格概要",
|
||||
"价格暂时不可用,请稍后重试": "Price temporarily unavailable, please try again later",
|
||||
@@ -431,11 +432,11 @@
|
||||
"余额充值管理": "残高チャージ管理",
|
||||
"作废": "無効化",
|
||||
"作废于": "無効化日",
|
||||
"下一次重置": "次回リセット",
|
||||
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "無効化するとこのサブスクリプションは直ちに失効します。履歴には影響しません。続行しますか?",
|
||||
"作用域": "スコープ",
|
||||
"作用域:包含分组": "スコープ:グループを含む",
|
||||
"作用域:包含模型名称": "スコープ:モデル名を含む",
|
||||
"开启后,模型名称会参与 cache key(不同模型隔离)。": "有効にすると、モデル名がキャッシュキーに含まれます(異なるモデルを分離)。",
|
||||
"作用域:包含规则名称": "スコープ:ルール名を含む",
|
||||
"你似乎并没有修改什么": "何も変更されていないようです",
|
||||
"你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "You can manually add them under “Custom model names”, click Fill and submit, or use the actions below to handle them automatically.",
|
||||
@@ -518,7 +519,7 @@
|
||||
"保存 Turnstile 设置": "Turnstile 設定を保存",
|
||||
"保存 WeChat Server 设置": "WeChatサーバー設定を保存",
|
||||
"保存分组倍率设置": "グループ倍率設定を保存",
|
||||
"保存分组相关设置": "グループ設定を保存",
|
||||
"保存分组相关设置": "グループ関連設定を保存",
|
||||
"保存备用码": "バックアップコード",
|
||||
"保存备用码以备不时之需": "万一に備え保存",
|
||||
"保存失败": "保存に失敗しました",
|
||||
@@ -570,6 +571,7 @@
|
||||
"允许 inference_geo 透传": "inference_geoパススルーを許可",
|
||||
"允许 safety_identifier 透传": "safety_identifierのパススルーを許可する",
|
||||
"允许 service_tier 透传": "service_tierのパススルーを許可する",
|
||||
"允许 speed 透传": "speed パススルーを許可",
|
||||
"允许 stream_options.include_obfuscation 透传": "stream_options.include_obfuscationパススルーを許可",
|
||||
"允许不安全的 Origin(HTTP)": "安全でないオリジン(HTTP)を許可する",
|
||||
"允许回调(会泄露服务器 IP 地址)": "コールバックを許可する(サーバーIPアドレスが漏洩します)",
|
||||
@@ -666,7 +668,7 @@
|
||||
"其他": "その他",
|
||||
"其他注册选项": "その他のサインアップオプション",
|
||||
"其他登录选项": "その他のログインオプション",
|
||||
"其他设置": "その他の設定",
|
||||
"其他设置": "その他",
|
||||
"其他详情": "Other details",
|
||||
"内存 阈值 (%)": "メモリしきい値 (%)",
|
||||
"内存使用率超过此值时拒绝请求": "メモリ使用率がこの値を超えた場合にリクエストを拒否",
|
||||
@@ -687,7 +689,7 @@
|
||||
"分类名称": "分類名称",
|
||||
"分组": "グループ",
|
||||
"分组JSON设置": "グループJSON設定",
|
||||
"分组与模型定价设置": "グループとモデルの料金設定",
|
||||
"分组与模型定价设置": "グループ&モデル料金設定",
|
||||
"分组价格": "グループ料金",
|
||||
"分组倍率": "グループレート",
|
||||
"分组倍率设置": "グループ倍率設定",
|
||||
@@ -761,6 +763,7 @@
|
||||
"刷新统计": "統計を更新",
|
||||
"刷新缓存统计": "キャッシュ統計を更新",
|
||||
"刷新缓存统计失败": "キャッシュ統計の更新に失敗しました",
|
||||
"刷新页面": "ページを更新",
|
||||
"前往 io.net API Keys": "Go to io.net API Keys",
|
||||
"前往设置": "Go to Settings",
|
||||
"前往设置页面": "Go to Settings Page",
|
||||
@@ -812,6 +815,8 @@
|
||||
"原密码": "現在のパスワード",
|
||||
"原生格式": "ネイティブ形式",
|
||||
"原生额度": "生クォータ",
|
||||
"使用原生额度输入": "生クォータで入力",
|
||||
"收起原生额度输入": "生クォータ入力を非表示",
|
||||
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "重複排除完了:重複排除前 {{before}} 個のAPIキー、重複排除後 {{after}} 個のAPIキー",
|
||||
"参与官方同步": "公式との同期",
|
||||
"参数": "パラメータ",
|
||||
@@ -911,6 +916,7 @@
|
||||
"启用Gemini思考后缀适配": "Gemini思考サフィックスモードを有効にする",
|
||||
"启用Ping间隔": "Ping間隔を有効にする",
|
||||
"启用SMTP SSL": "SMTP SSLを有効にする",
|
||||
"强制使用 AUTH LOGIN": "AUTH LOGINを強制する",
|
||||
"启用SSRF防护(推荐开启以保护服务器安全)": "SSRF保護を有効にする(サーバーを保護するため、有効化を推奨します)",
|
||||
"启用供应商": "プロバイダーを有効化",
|
||||
"启用全部": "すべてを有効にする",
|
||||
@@ -1344,6 +1350,7 @@
|
||||
"开启后,将定期发送ping数据保持连接活跃": "有効にすると、接続をアクティブに保つためにpingデータが定期的に送信されます",
|
||||
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "有効にすると、現在のグループチャネルが失敗した場合、次のグループのチャネルを順番に試行します",
|
||||
"开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启": "有効にすると、すべてのリクエストは直接アップストリームにパススルーされ、いかなる処理も行われません(リダイレクトとチャネルの自動調整も無効になります)。有効にする際はご注意ください",
|
||||
"开启后,模型名称会参与 cache key(不同模型隔离)。": "有効にすると、モデル名がキャッシュキーに含まれます(異なるモデルを分離)。",
|
||||
"开启后,若该规则命中且请求失败,将不会切换渠道重试。": "有効にすると、このルールがヒットしてリクエストが失敗した場合、チャネル切り替えリトライは行われません。",
|
||||
"开启后,规则名称会参与 cache key(不同规则隔离)。": "有効にすると、ルール名がキャッシュキーに含まれます(ルールごとに隔離)。",
|
||||
"开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)": "有効にすると、このチャネルでClaudeにリクエストする際に?beta=trueが強制追加されます(クライアント側で手動パラメータ渡し不要)",
|
||||
@@ -1418,7 +1425,7 @@
|
||||
"思考预算占比": "思考予算の割合",
|
||||
"性能指标": "性能指標",
|
||||
"性能监控": "パフォーマンス監視",
|
||||
"性能设置": "パフォーマンス設定",
|
||||
"性能设置": "パフォーマンス",
|
||||
"总 GPU 小时": "Total GPU Hours",
|
||||
"总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}": "合計料金:テキスト料金 {{textPrice}} + オーディオ料金 {{audioPrice}} = {{symbol}}{{total}}",
|
||||
"总分配内存": "総割り当てメモリ",
|
||||
@@ -1567,7 +1574,7 @@
|
||||
"支付方式名称": "決済方法名",
|
||||
"支付方式类型": "決済方法タイプ",
|
||||
"支付渠道": "決済チャネル",
|
||||
"支付设置": "決済設定",
|
||||
"支付设置": "決済",
|
||||
"支付请求失败": "決済リクエストに失敗しました",
|
||||
"支付金额": "決済金額",
|
||||
"支持 Ctrl+V 粘贴图片": "Ctrl+V で画像を貼り付け可能",
|
||||
@@ -1867,6 +1874,7 @@
|
||||
"条件规则": "条件ルール",
|
||||
"条件项设置": "条件項目設定",
|
||||
"条日志已清理!": "件のログがクリアされました",
|
||||
"条规则": "件のルール",
|
||||
"条,共": "件、合計",
|
||||
"来源": "ソース",
|
||||
"来源于 IO.NET 部署": "From IO.NET Deployment",
|
||||
@@ -1952,6 +1960,7 @@
|
||||
"模型定价,需要登录访问": "モデル料金(アクセスにはログインが必要です)",
|
||||
"模型广场": "モデルマーケットプレイス",
|
||||
"模型拉取失败: {{error}}": "Failed to pull model: {{error}}",
|
||||
"模型排行": "モデルランキング",
|
||||
"模型支持的接口端点信息": "モデルが対応するAPIエンドポイント情報",
|
||||
"模型数据分析": "モデルデータ分析",
|
||||
"模型映射必须是合法的 JSON 格式!": "モデルマッピングは、有効なJSON形式である必要があります",
|
||||
@@ -1964,7 +1973,7 @@
|
||||
"模型消耗趋势": "モデル消費推移",
|
||||
"模型版本": "モデルバージョン",
|
||||
"模型的详细描述和基本特性": "モデルの詳細な説明と基本的な特徴",
|
||||
"模型相关设置": "モデル関連設定",
|
||||
"模型相关设置": "モデル関連",
|
||||
"模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "モデルコミュニティは皆様の協力によって維持されています。データに誤りがある場合や、新規モデルデータをコントリビュートしたい場合は、以下にアクセスしてください:",
|
||||
"模型管理": "モデル管理",
|
||||
"模型组": "モデルグループ",
|
||||
@@ -1977,7 +1986,7 @@
|
||||
"模型部署": "Model Deployment",
|
||||
"模型部署服务未启用": "Model deployment service is not enabled",
|
||||
"模型部署管理": "Model Deployment Management",
|
||||
"模型部署设置": "Model Deployment Settings",
|
||||
"模型部署设置": "モデルデプロイ",
|
||||
"模型配置": "モデル設定",
|
||||
"模型重定向": "モデルマッピング",
|
||||
"模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "The following models from the redirect have not been added to the “Models” list and requests will fail due to no available model:",
|
||||
@@ -2107,6 +2116,7 @@
|
||||
"添加公告": "お知らせ追加",
|
||||
"添加分类": "分類追加",
|
||||
"添加分组": "グループを追加",
|
||||
"添加分组规则": "グループルールを追加",
|
||||
"添加后提交": "Submit after adding",
|
||||
"添加启动参数": "Add Startup Args",
|
||||
"添加启动命令": "Add Startup Command",
|
||||
@@ -2122,6 +2132,14 @@
|
||||
"添加键值对": "キー/値ペア追加",
|
||||
"添加问答": "FAQ追加",
|
||||
"添加额度": "残高追加",
|
||||
"减少": "減少",
|
||||
"覆盖": "上書き",
|
||||
"调整额度": "残高調整",
|
||||
"调整额度成功": "残高の調整に成功しました",
|
||||
"当前额度": "現在の残高",
|
||||
"变更": "変更",
|
||||
"预计结果": "予想結果",
|
||||
"正数为增加,负数为减少": "正の数で追加、負の数で減少",
|
||||
"清理不活跃缓存": "非アクティブなキャッシュをクリーンアップ",
|
||||
"清理失败": "クリーンアップに失敗しました",
|
||||
"清理方式": "クリーンアップモード",
|
||||
@@ -2237,6 +2255,8 @@
|
||||
"用户每周期最多请求完成次数": "期間ごとのユーザー最大成功リクエスト数",
|
||||
"用户每周期最多请求次数": "期間ごとのユーザー最大リクエスト数",
|
||||
"用户注册时看到的网站名称,比如'我的网站'": "ユーザーがサインアップ時に表示されるウェブサイト名です。例:「マイサイト」",
|
||||
"用户消耗排行": "ユーザー消費ランキング",
|
||||
"用户消耗趋势": "ユーザー消費推移",
|
||||
"用户的基本账户信息": "ユーザーの基本アカウント情報",
|
||||
"用户管理": "ユーザー管理",
|
||||
"用户组": "ユーザーグループ",
|
||||
@@ -2326,6 +2346,7 @@
|
||||
"确认冲突项修改": "競合項目の変更の確認",
|
||||
"确认删除": "削除の確認",
|
||||
"确认删除模型": "Confirm Delete Model",
|
||||
"确认删除该分组的所有规则?": "このグループの全ルールを削除しますか?",
|
||||
"确认删除该分组?": "このグループを削除しますか?",
|
||||
"确认删除该规则?": "このルールを削除しますか?",
|
||||
"确认取消密码登录": "パスワードログイン無効化の確認",
|
||||
@@ -2479,7 +2500,7 @@
|
||||
"系统文档和帮助信息": "システムのドキュメントとヘルプ",
|
||||
"系统消息": "システムメッセージ",
|
||||
"系统管理功能": "システム管理機能",
|
||||
"系统设置": "システム設定",
|
||||
"系统设置": "システム",
|
||||
"系统访问令牌": "システムアクセストークン",
|
||||
"索引": "インデックス",
|
||||
"紧凑列表": "コンパクトリスト",
|
||||
@@ -2508,7 +2529,7 @@
|
||||
"绘图": "画像生成",
|
||||
"绘图任务记录": "画像生成タスク履歴",
|
||||
"绘图日志": "画像生成履歴",
|
||||
"绘图设置": "画像生成設定",
|
||||
"绘图设置": "画像生成",
|
||||
"统一的": "統合型",
|
||||
"统计Tokens": "トークン統計",
|
||||
"统计已重置": "統計がリセットされました",
|
||||
@@ -2585,7 +2606,7 @@
|
||||
"聊天区域": "チャットエリア",
|
||||
"聊天应用名称": "チャットアプリ名",
|
||||
"聊天应用名称已存在,请使用其他名称": "このチャットアプリ名はすでに存在します。別の名称を入力してください",
|
||||
"聊天设置": "チャット設定",
|
||||
"聊天设置": "チャット",
|
||||
"聊天配置": "チャット設定",
|
||||
"聊天链接配置错误,请联系管理员": "チャットURLの設定でエラーが発生しました。管理者にお問い合わせください",
|
||||
"联系我们": "お問い合わせ",
|
||||
@@ -2834,6 +2855,7 @@
|
||||
"请求参数无效": "Invalid request parameters",
|
||||
"请求发生错误": "リクエストでエラーが発生しました",
|
||||
"请求发生错误: ": "リクエストでエラーが発生しました:",
|
||||
"模型价格未配置": "モデル価格が未設定",
|
||||
"请求后端接口失败:": "バックエンドAPIリクエストに失敗しました:",
|
||||
"请求失败": "リクエストに失敗しました",
|
||||
"请求头覆盖": "リクエストヘッダーの上書き",
|
||||
@@ -3021,9 +3043,6 @@
|
||||
"调用次数分布": "呼び出し回数分布",
|
||||
"调用次数排行": "呼び出し回数ランキング",
|
||||
"调用趋势": "呼び出し推移",
|
||||
"模型排行": "モデルランキング",
|
||||
"用户消耗排行": "ユーザー消費ランキング",
|
||||
"用户消耗趋势": "ユーザー消費推移",
|
||||
"调试信息": "デバッグ情報",
|
||||
"谨慎": "注意",
|
||||
"豆包": "豆包",
|
||||
@@ -3114,7 +3133,7 @@
|
||||
"过期时间不能早于当前时间!": "有効期限は現在時刻より前に設定できません",
|
||||
"过期时间快捷设置": "有効期限クイック設定",
|
||||
"过期时间格式错误!": "有効期限のフォーマットが正しくありません",
|
||||
"运营设置": "運用設定",
|
||||
"运营设置": "運用",
|
||||
"运行中": "Running",
|
||||
"运行命令 (Command)": "Command",
|
||||
"运行时长": "Runtime Duration",
|
||||
@@ -3200,7 +3219,7 @@
|
||||
"通道 ${name} 余额更新成功!": "チャネル「${name}」のクォータを更新しました。",
|
||||
"通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。": "チャネル「${name}」のテストに成功しました。モデル「${model}」の所要時間 ${time.toFixed(2)} 秒。",
|
||||
"通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "チャネル「${name}」のテストに成功しました。所要時間 ${time.toFixed(2)} 秒。",
|
||||
"速率限制设置": "レート制限設定",
|
||||
"速率限制设置": "レート制限",
|
||||
"逻辑": "ロジック",
|
||||
"邀请": "招待",
|
||||
"邀请人": "招待元",
|
||||
@@ -3363,6 +3382,7 @@
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音声出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 音声倍率 {{audioRatio}} * 音声補完倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"页脚": "フッター",
|
||||
"页面未找到,请检查您的浏览器地址是否正确": "ページが見つかりませんでした。ブラウザのアドレスが正しいかご確認ください",
|
||||
"页面渲染出错,请刷新页面重试": "ページのレンダリング中にエラーが発生しました。ページを更新して再試行してください。",
|
||||
"顶栏管理": "トップバー管理",
|
||||
"项": "件",
|
||||
"项目": "プロジェクト",
|
||||
@@ -3429,140 +3449,6 @@
|
||||
"默认测试模型": "デフォルトテストモデル",
|
||||
"默认用户消息": "こんにちは",
|
||||
"默认补全倍率": "デフォルト補完倍率",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "注意: エンドポイントマッピングは「モデル広場」での表示専用で、実際の呼び出しには影響しません。実際の呼び出し設定は「チャネル管理」で行ってください。",
|
||||
"购买订阅获得模型额度/次数": "サブスクリプション購入でモデルのクォータ/回数を取得",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "本番環境 RSA 秘密鍵 Base64 (PKCS#8 DER)",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "サンドボックス RSA 秘密鍵 Base64 (PKCS#8 DER)",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "本番環境 Waffo 公開鍵 Base64 (X.509 DER)",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "サンドボックス Waffo 公開鍵 Base64 (X.509 DER)",
|
||||
"支付方式类型": "決済方法タイプ",
|
||||
"支付方式名称": "決済方法名",
|
||||
"获取充值配置失败": "チャージ設定の取得に失敗しました",
|
||||
"获取充值配置异常": "チャージ設定エラー",
|
||||
"分组相关设置": "グループ関連設定",
|
||||
"保存分组相关设置": "グループ関連設定を保存",
|
||||
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "このページには価格または基本倍率が未設定のモデルのみ表示され、設定後は一覧から自動的に消えます。",
|
||||
"没有未设置定价的模型": "価格未設定のモデルはありません",
|
||||
"当前没有未设置定价的模型": "現在、価格未設定のモデルはありません",
|
||||
"模型计费编辑器": "モデル料金エディタ",
|
||||
"价格摘要": "価格概要",
|
||||
"当前提示": "現在のヒント",
|
||||
"这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。": "この画面では価格を基準に入力し、保存時にバックエンドが必要とする倍率 JSON に自動変換されます。",
|
||||
"当前未启用,需要时再打开即可。": "この項目は現在無効です。必要なときに有効にしてください。",
|
||||
"下面展示这个模型保存后会写入哪些后端字段,便于和原始 JSON 编辑框保持一致。": "保存後にこのモデルでどのバックエンド項目に書き込まれるかを以下に表示します。元の JSON エディタとの整合確認に便利です。",
|
||||
"补全价格已锁定": "補完価格はロックされています",
|
||||
"后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "バックエンド固定倍率: {{ratio}}。この項目は変換後の価格表示のみです。",
|
||||
"这些价格都是可选项,不填也可以。": "これらの価格はすべて任意項目で、未入力でも構いません。",
|
||||
"请先开启并填写音频输入价格。": "先に音声入力価格を有効にして入力してください。",
|
||||
"输入模型名称,例如 gpt-4.1": "モデル名を入力してください。例: gpt-4.1",
|
||||
"当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。": "このモデルには従量価格と倍率設定が同時に存在しています。保存すると現在の課金方式に従って上書きされます。",
|
||||
"当前模型存在未显式设置输入倍率的扩展倍率;填写输入价格后会自动换算为价格字段。": "このモデルには入力倍率が明示されていない拡張倍率があります。入力価格を設定すると価格項目へ自動換算されます。",
|
||||
"按量计费下需要先填写输入价格,才能保存其它价格项。": "従量課金では、他の価格項目を保存する前に入力価格を設定する必要があります。",
|
||||
"填写音频补全价格前,需要先填写音频输入价格。": "音声補完価格を入力する前に、先に音声入力価格を入力してください。",
|
||||
"模型 {{name}} 缺少输入价格,无法计算补全/缓存/图片/音频价格对应的倍率": "モデル {{name}} に入力価格がないため、補完・キャッシュ・画像・音声価格に対応する倍率を計算できません。",
|
||||
"模型 {{name}} 缺少音频输入价格,无法计算音频补全倍率": "モデル {{name}} に音声入力価格がないため、音声補完倍率を計算できません。",
|
||||
"批量应用当前模型价格": "現在のモデル価格を一括適用",
|
||||
"请先选择一个作为模板的模型": "まずテンプレートとして使うモデルを選択してください",
|
||||
"请先勾选需要批量设置的模型": "一括設定したいモデルを先に選択してください",
|
||||
"已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型": "モデル {{name}} の価格設定を {{count}} 個のモデルに一括適用しました",
|
||||
"将把当前编辑中的模型 {{name}} 的价格配置,批量应用到已勾选的 {{count}} 个模型。": "現在編集中のモデル {{name}} の価格設定を、選択済みの {{count}} 個のモデルに一括適用します。",
|
||||
"适合同系列模型一起定价,例如把 gpt-5.1 的价格批量同步到 gpt-5.1-high、gpt-5.1-low 等模型。": "同系列モデルをまとめて価格設定するのに適しています。例えば gpt-5.1 の価格を gpt-5.1-high、gpt-5.1-low などへ一括同期できます。",
|
||||
"已勾选": "選択済み",
|
||||
"当前编辑": "編集中",
|
||||
"已勾选 {{count}} 个模型": "{{count}} 個のモデルを選択済み",
|
||||
"计费方式": "課金方式",
|
||||
"未设置价格": "価格未設定",
|
||||
"保存预览": "保存プレビュー",
|
||||
"基础价格": "基本価格",
|
||||
"扩展价格": "追加価格",
|
||||
"额外价格项": "追加価格項目",
|
||||
"补全价格": "補完価格",
|
||||
"缓存读取价格": "入力キャッシュ読み取り価格",
|
||||
"缓存创建价格": "入力キャッシュ作成価格",
|
||||
"图片输入价格": "画像入力価格",
|
||||
"音频输入价格": "音声入力価格",
|
||||
"音频补全价格": "音声補完価格",
|
||||
"适合 MJ / 任务类等按次收费模型。": "MJ やその他のリクエスト単位課金モデルに適しています。",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "このモデルの補完倍率はバックエンドで {{ratio}} に固定されています。ここでは補完価格を変更できません。",
|
||||
"计费显示模式": "課金表示モード",
|
||||
"价格模式(默认)": "価格モード(デフォルト)",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "モデル価格 {{symbol}}{{price}} / リクエスト",
|
||||
"按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "リクエストごと {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "モデル価格:{{symbol}}{{price}} / リクエスト",
|
||||
"按次:{{symbol}}{{price}}": "リクエストごと:{{symbol}}{{price}}",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "実際の請求額:{{symbol}}{{total}}(グループ価格調整込み)",
|
||||
"缓存读取价格:{{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}}{{price}} / 1M tokens": "キャッシュ作成価格 {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "5m キャッシュ作成価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "5m キャッシュ作成価格 {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "1h キャッシュ作成価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "1h キャッシュ作成価格 {{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}}{{price}} / 1M tokens",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "音声入力価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "音声補完価格:{{symbol}}{{price}} / 1M tokens",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Web 検索呼び出し {{webSearchCallCount}} 回",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "ファイル検索呼び出し {{fileSearchCallCount}} 回",
|
||||
"图片倍率 {{imageRatio}}": "画像倍率 {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "音声倍率 {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "通常入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "キャッシュ入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * キャッシュ倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "画像入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 画像倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音声入力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 音声倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 補完倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web 検索: {{count}} / 1K * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "ファイル検索: {{count}} / 1K * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "画像生成: 1 回 * 単価 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "合計: {{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "モデル倍率 {{modelRatio}}、補完倍率 {{completionRatio}}、音声倍率 {{audioRatio}}、音声補完倍率 {{audioCompletionRatio}}、{{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "テキスト出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 補完倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音声出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 音声倍率 {{audioRatio}} * 音声補完倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "合計: テキスト部分 {{textTotal}} + 音声部分 {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "モデル倍率 {{modelRatio}}、出力倍率 {{completionRatio}}、キャッシュ倍率 {{cacheRatio}}、{{ratioType}} {{ratio}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "キャッシュ読み取り: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * キャッシュ倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * キャッシュ作成倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "5m キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 5m キャッシュ作成倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h キャッシュ作成: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 1h キャッシュ作成倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "出力: {{tokens}} / 1M * モデル倍率 {{modelRatio}} * 出力倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "空",
|
||||
"{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x",
|
||||
"模型价格:{{symbol}}{{price}}": "モデル価格:{{symbol}}{{price}}",
|
||||
"模型价格 {{price}}": "モデル価格 {{price}}",
|
||||
"缓存读 {{price}} / 1M tokens": "キャッシュ読み取り {{price}} / 1M tokens",
|
||||
"5m缓存创建 {{price}} / 1M tokens": "5m キャッシュ作成 {{price}} / 1M tokens",
|
||||
"1h缓存创建 {{price}} / 1M tokens": "1h キャッシュ作成 {{price}} / 1M tokens",
|
||||
"缓存创建 {{price}} / 1M tokens": "キャッシュ作成 {{price}} / 1M tokens",
|
||||
"图片输入 {{price}} / 1M tokens": "画像入力 {{price}} / 1M tokens",
|
||||
"输入 {{price}} / 1M tokens": "入力 {{price}} / 1M tokens",
|
||||
"缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "キャッシュ {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "キャッシュ作成 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "5m キャッシュ作成 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "1h キャッシュ作成 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}": "(入力 {{nonImageInput}} tokens + 画像入力 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"图片输入价格:{{symbol}}{{total}} / 1M tokens": "画像入力価格:{{symbol}}{{total}} / 1M tokens",
|
||||
"文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "テキストプロンプト {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + テキスト補完 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音声プロンプト {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音声補完 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "モデル価格 {{symbol}}{{price}} / リクエスト * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"缓存读取价格:{{symbol}}{{total}} / 1M tokens": "キャッシュ読み取り価格:{{symbol}}{{total}} / 1M tokens",
|
||||
"补全 {{completion}} tokens * 输出倍率 {{completionRatio}}": "補完 {{completion}} tokens * 出力倍率 {{completionRatio}}",
|
||||
"补全倍率 {{completionRatio}}": "補完倍率 {{completionRatio}}",
|
||||
"输入价格:{{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",
|
||||
"复制密钥": "キーをコピー",
|
||||
"复制连接信息": "接続情報をコピー",
|
||||
"检测到剪贴板中的连接信息": "クリップボードに接続情報が検出されました",
|
||||
"自动填入": "自動入力",
|
||||
"忽略": "無視",
|
||||
"从剪贴板粘贴配置": "クリップボードから貼り付け",
|
||||
"剪贴板中未检测到连接信息": "クリップボードに接続情報が見つかりません",
|
||||
"连接信息已填入": "接続情報を入力しました",
|
||||
"无法读取剪贴板": "クリップボードを読み取れません",
|
||||
"页面渲染出错,请刷新页面重试": "ページのレンダリング中にエラーが発生しました。ページを更新して再試行してください。",
|
||||
"刷新页面": "ページを更新",
|
||||
"(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(現在、Epay APIのみに対応しています。デフォルトで、上記のサーバーURLがコールバックアドレスとして使用されます。)",
|
||||
",当前无生效订阅,将自动使用钱包": "、有効なサブスクリプションがないため、自動的にウォレットを使用します",
|
||||
",时间:": "、時間:",
|
||||
|
||||
Vendored
+36
-150
@@ -249,6 +249,7 @@
|
||||
"price_xxx 的商品价格 ID,新建产品后可获得": "ID цены товара price_xxx, можно получить после создания нового продукта",
|
||||
"safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "Поле safety_identifier помогает OpenAI идентифицировать пользователей приложений, которые могут нарушать политику использования. По умолчанию отключено для защиты конфиденциальности пользователей",
|
||||
"service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "Поле service_tier используется для указания уровня сервиса, позволяет передавать параметры, которые могут привести к фактической оплате выше ожидаемой. По умолчанию отключено для избежания дополнительных расходов",
|
||||
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Поле speed управляет режимом скорости инференса Claude. По умолчанию отключено, чтобы избежать непреднамеренного переключения в режим fast",
|
||||
"sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Ключ Stripe sk_xxx или rk_xxx, конфиденциальная информация не отображается",
|
||||
"standard 已被移除,vip 用户看不到": "standard has been removed, vip users cannot see it",
|
||||
"store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "Поле store используется для авторизации OpenAI хранить данные запросов для оценки и оптимизации продукта. По умолчанию отключено, после включения может привести к неработоспособности Codex",
|
||||
@@ -408,7 +409,7 @@
|
||||
"以下上游数据可能不可信:": "Следующие upstream данные могут быть недостоверными:",
|
||||
"以下文件解析失败,已忽略:{{list}}": "Не удалось проанализировать следующие файлы, они проигнорированы: {{list}}",
|
||||
"以及": "а также",
|
||||
"仪表盘设置": "Настройки панели управления",
|
||||
"仪表盘设置": "Панель управления",
|
||||
"价格": "Цена",
|
||||
"价格摘要": "Сводка цен",
|
||||
"价格暂时不可用,请稍后重试": "Price temporarily unavailable, please try again later",
|
||||
@@ -438,11 +439,11 @@
|
||||
"余额充值管理": "Управление пополнением баланса",
|
||||
"作废": "Аннулировать",
|
||||
"作废于": "Аннулировано",
|
||||
"下一次重置": "Следующий сброс",
|
||||
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "После аннулирования подписка сразу станет недействительной. История не изменится. Продолжить?",
|
||||
"作用域": "Область действия",
|
||||
"作用域:包含分组": "Область действия: включить группу",
|
||||
"作用域:包含模型名称": "Область действия: включить имя модели",
|
||||
"开启后,模型名称会参与 cache key(不同模型隔离)。": "При включении имя модели включается в ключ кэша (изолирует разные модели).",
|
||||
"作用域:包含规则名称": "Область действия: включить имя правила",
|
||||
"你似乎并没有修改什么": "Похоже, вы ничего не изменили",
|
||||
"你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "Вы можете добавить их вручную в разделе «Пользовательские названия моделей», нажать «Заполнить», затем отправить или воспользоваться действиями ниже для автоматической обработки.",
|
||||
@@ -577,6 +578,7 @@
|
||||
"允许 inference_geo 透传": "Разрешить передачу inference_geo",
|
||||
"允许 safety_identifier 透传": "Разрешить сквозную передачу safety_identifier",
|
||||
"允许 service_tier 透传": "Разрешить сквозную передачу service_tier",
|
||||
"允许 speed 透传": "Разрешить передачу speed",
|
||||
"允许 stream_options.include_obfuscation 透传": "Разрешить передачу stream_options.include_obfuscation",
|
||||
"允许不安全的 Origin(HTTP)": "Разрешить небезопасные Origin (HTTP)",
|
||||
"允许回调(会泄露服务器 IP 地址)": "Разрешить обратные вызовы (может раскрыть IP-адрес сервера)",
|
||||
@@ -681,7 +683,7 @@
|
||||
"其他": "Другое",
|
||||
"其他注册选项": "Другие варианты регистрации",
|
||||
"其他登录选项": "Другие варианты входа",
|
||||
"其他设置": "Другие настройки",
|
||||
"其他设置": "Прочее",
|
||||
"其他详情": "Другие детали",
|
||||
"内存 阈值 (%)": "Порог памяти (%)",
|
||||
"内存使用率超过此值时拒绝请求": "Отклонять запросы, когда использование памяти превышает это значение",
|
||||
@@ -702,7 +704,7 @@
|
||||
"分类名称": "Название категории",
|
||||
"分组": "Группа",
|
||||
"分组JSON设置": "Group JSON Settings",
|
||||
"分组与模型定价设置": "Настройки групп и ценообразования моделей",
|
||||
"分组与模型定价设置": "Группы и цены моделей",
|
||||
"分组价格": "Цена группы",
|
||||
"分组倍率": "Коэффициент группы",
|
||||
"分组倍率设置": "Настройки коэффициента группы",
|
||||
@@ -776,6 +778,7 @@
|
||||
"刷新统计": "Обновить статистику",
|
||||
"刷新缓存统计": "Обновить статистику кэша",
|
||||
"刷新缓存统计失败": "Не удалось обновить статистику кэша",
|
||||
"刷新页面": "Обновить страницу",
|
||||
"前往 io.net API Keys": "Go to io.net API Keys",
|
||||
"前往设置": "Go to Settings",
|
||||
"前往设置页面": "Go to Settings Page",
|
||||
@@ -827,6 +830,8 @@
|
||||
"原密码": "Старый пароль",
|
||||
"原生格式": "Нативный формат",
|
||||
"原生额度": "Исходный лимит",
|
||||
"使用原生额度输入": "Ввод в исходных единицах",
|
||||
"收起原生额度输入": "Скрыть ввод в исходных единицах",
|
||||
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Дедупликация завершена: до дедупликации {{before}} ключей, после дедупликации {{after}} ключей",
|
||||
"参与官方同步": "Участвовать в официальной синхронизации",
|
||||
"参数": "Параметры",
|
||||
@@ -926,6 +931,7 @@
|
||||
"启用Gemini思考后缀适配": "Включить адаптацию суффикса мышления Gemini",
|
||||
"启用Ping间隔": "Включить интервал Ping",
|
||||
"启用SMTP SSL": "Включить SMTP SSL",
|
||||
"强制使用 AUTH LOGIN": "Принудительно AUTH LOGIN",
|
||||
"启用SSRF防护(推荐开启以保护服务器安全)": "Включить защиту SSRF (рекомендуется включить для защиты безопасности сервера)",
|
||||
"启用供应商": "Включить поставщика",
|
||||
"启用全部": "Включить все",
|
||||
@@ -1373,6 +1379,7 @@
|
||||
"开启后,将定期发送ping数据保持连接活跃": "После включения будет периодически отправляться ping-данные для поддержания активности соединения",
|
||||
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "После включения, когда канал текущей группы не работает, он будет пытаться использовать канал следующей группы по порядку",
|
||||
"开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启": "После включения все запросы будут напрямую передаваться upstream без какой-либо обработки (перенаправление и адаптация каналов также будут отключены), включайте с осторожностью",
|
||||
"开启后,模型名称会参与 cache key(不同模型隔离)。": "При включении имя модели включается в ключ кэша (изолирует разные модели).",
|
||||
"开启后,若该规则命中且请求失败,将不会切换渠道重试。": "При включении, если правило сработало и запрос не удался, переключение канала для повтора не выполняется.",
|
||||
"开启后,规则名称会参与 cache key(不同规则隔离)。": "При включении имя правила будет частью ключа кэша (изоляция по правилам).",
|
||||
"开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)": "При включении запросы к Claude через этот канал будут принудительно дополнены ?beta=true (клиенту не нужно передавать этот параметр вручную)",
|
||||
@@ -1447,7 +1454,7 @@
|
||||
"思考预算占比": "Доля бюджета на размышления",
|
||||
"性能指标": "Показатели производительности",
|
||||
"性能监控": "Мониторинг производительности",
|
||||
"性能设置": "Настройки производительности",
|
||||
"性能设置": "Производительность",
|
||||
"总 GPU 小时": "Total GPU Hours",
|
||||
"总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}": "Общая цена: цена текста {{textPrice}} + цена аудио {{audioPrice}} = {{symbol}}{{total}}",
|
||||
"总分配内存": "Общая выделенная память",
|
||||
@@ -1596,7 +1603,7 @@
|
||||
"支付方式名称": "Название метода оплаты",
|
||||
"支付方式类型": "Тип метода оплаты",
|
||||
"支付渠道": "Платежные каналы",
|
||||
"支付设置": "Настройки оплаты",
|
||||
"支付设置": "Оплата",
|
||||
"支付请求失败": "Запрос на оплату не удался",
|
||||
"支付金额": "Сумма оплаты",
|
||||
"支持 Ctrl+V 粘贴图片": "Поддержка Ctrl+V для вставки изображения",
|
||||
@@ -1896,6 +1903,7 @@
|
||||
"条件规则": "Правила условий",
|
||||
"条件项设置": "Настройки элементов условий",
|
||||
"条日志已清理!": "записей журнала очищено!",
|
||||
"条规则": "rules",
|
||||
"条,共": "записей, всего",
|
||||
"来源": "Источник",
|
||||
"来源于 IO.NET 部署": "From IO.NET Deployment",
|
||||
@@ -1981,6 +1989,7 @@
|
||||
"模型定价,需要登录访问": "Ценообразование моделей, требуется вход для доступа",
|
||||
"模型广场": "Площадка моделей",
|
||||
"模型拉取失败: {{error}}": "Failed to pull model: {{error}}",
|
||||
"模型排行": "Рейтинг моделей",
|
||||
"模型支持的接口端点信息": "Информация о конечных точках интерфейса, поддерживаемых моделью",
|
||||
"模型数据分析": "Анализ данных моделей",
|
||||
"模型映射必须是合法的 JSON 格式!": "Сопоставление моделей должно быть в допустимом формате JSON!",
|
||||
@@ -1993,7 +2002,7 @@
|
||||
"模型消耗趋势": "Тенденции потребления моделей",
|
||||
"模型版本": "Версия модели",
|
||||
"模型的详细描述和基本特性": "Подробное описание и основные характеристики модели",
|
||||
"模型相关设置": "Настройки, связанные с моделью",
|
||||
"模型相关设置": "Модели",
|
||||
"模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "Сообщество моделей требует совместного поддержания всеми. Если вы обнаружили ошибки в данных или хотите внести новые данные о моделях, посетите:",
|
||||
"模型管理": "Управление моделями",
|
||||
"模型组": "Группа моделей",
|
||||
@@ -2006,7 +2015,7 @@
|
||||
"模型部署": "Model Deployment",
|
||||
"模型部署服务未启用": "Model deployment service is not enabled",
|
||||
"模型部署管理": "Model Deployment Management",
|
||||
"模型部署设置": "Model Deployment Settings",
|
||||
"模型部署设置": "Развёртывание моделей",
|
||||
"模型配置": "Конфигурация модели",
|
||||
"模型重定向": "Перенаправление модели",
|
||||
"模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "Следующие модели из перенаправления ещё не добавлены в список «Модели», из-за отсутствия доступных моделей вызовы завершатся ошибкой:",
|
||||
@@ -2136,6 +2145,7 @@
|
||||
"添加公告": "Добавить объявление",
|
||||
"添加分类": "Добавить категорию",
|
||||
"添加分组": "Add Group",
|
||||
"添加分组规则": "Add Group Rules",
|
||||
"添加后提交": "Отправить после добавления",
|
||||
"添加启动参数": "Add Startup Args",
|
||||
"添加启动命令": "Add Startup Command",
|
||||
@@ -2151,6 +2161,14 @@
|
||||
"添加键值对": "Добавить пару ключ-значение",
|
||||
"添加问答": "Добавить вопрос-ответ",
|
||||
"添加额度": "Добавить лимит",
|
||||
"减少": "Уменьшить",
|
||||
"覆盖": "Заменить",
|
||||
"调整额度": "Скорректировать квоту",
|
||||
"调整额度成功": "Квота успешно скорректирована",
|
||||
"当前额度": "Текущая квота",
|
||||
"变更": "Изменение",
|
||||
"预计结果": "Ожидаемый результат",
|
||||
"正数为增加,负数为减少": "Положительное для увеличения, отрицательное для уменьшения",
|
||||
"清理不活跃缓存": "Очистить неактивный кэш",
|
||||
"清理失败": "Ошибка очистки",
|
||||
"清理方式": "Режим очистки",
|
||||
@@ -2266,6 +2284,8 @@
|
||||
"用户每周期最多请求完成次数": "Максимальное количество выполненных запросов пользователя за период",
|
||||
"用户每周期最多请求次数": "Максимальное количество запросов пользователя за период",
|
||||
"用户注册时看到的网站名称,比如'我的网站'": "Название сайта, которое видят пользователи при регистрации, например 'Мой сайт'",
|
||||
"用户消耗排行": "Рейтинг потребления пользователей",
|
||||
"用户消耗趋势": "Тенденция потребления пользователей",
|
||||
"用户的基本账户信息": "Основная информация об аккаунте пользователя",
|
||||
"用户管理": "Управление пользователями",
|
||||
"用户组": "Группа пользователей",
|
||||
@@ -2359,6 +2379,7 @@
|
||||
"确认冲突项修改": "Подтвердить изменение конфликтующих элементов",
|
||||
"确认删除": "Подтвердить удаление",
|
||||
"确认删除模型": "Confirm Delete Model",
|
||||
"确认删除该分组的所有规则?": "Delete all rules for this group?",
|
||||
"确认删除该分组?": "Confirm delete this group?",
|
||||
"确认删除该规则?": "Confirm delete this rule?",
|
||||
"确认取消密码登录": "Подтвердить отмену входа по паролю",
|
||||
@@ -2512,7 +2533,7 @@
|
||||
"系统文档和帮助信息": "Системная документация и справочная информация",
|
||||
"系统消息": "Системные сообщения",
|
||||
"系统管理功能": "Функции системного управления",
|
||||
"系统设置": "Системные настройки",
|
||||
"系统设置": "Система",
|
||||
"系统访问令牌": "Токен доступа к системе",
|
||||
"索引": "Индекс",
|
||||
"紧凑列表": "Компактный список",
|
||||
@@ -2541,7 +2562,7 @@
|
||||
"绘图": "Рисование",
|
||||
"绘图任务记录": "Записи задач рисования",
|
||||
"绘图日志": "Журнал рисования",
|
||||
"绘图设置": "Настройки рисования",
|
||||
"绘图设置": "Рисование",
|
||||
"统一的": "Единый",
|
||||
"统计Tokens": "Статистика токенов",
|
||||
"统计已重置": "Статистика сброшена",
|
||||
@@ -2618,7 +2639,7 @@
|
||||
"聊天区域": "Область чата",
|
||||
"聊天应用名称": "Название чат-приложения",
|
||||
"聊天应用名称已存在,请使用其他名称": "Название чат-приложения уже существует, используйте другое название",
|
||||
"聊天设置": "Настройки чата",
|
||||
"聊天设置": "Чат",
|
||||
"聊天配置": "Конфигурация чата",
|
||||
"聊天链接配置错误,请联系管理员": "Ошибка конфигурации ссылки чата, свяжитесь с администратором",
|
||||
"联系我们": "Свяжитесь с нами",
|
||||
@@ -2867,6 +2888,7 @@
|
||||
"请求参数无效": "Invalid request parameters",
|
||||
"请求发生错误": "Произошла ошибка запроса",
|
||||
"请求发生错误: ": "Произошла ошибка запроса: ",
|
||||
"模型价格未配置": "Цена модели не настроена",
|
||||
"请求后端接口失败:": "Не удалось запросить внутренний интерфейс:",
|
||||
"请求失败": "Запрос не удался",
|
||||
"请求头覆盖": "Переопределение заголовков запроса",
|
||||
@@ -3054,9 +3076,6 @@
|
||||
"调用次数分布": "Распределение количества вызовов",
|
||||
"调用次数排行": "Рейтинг количества вызовов",
|
||||
"调用趋势": "Тенденция вызовов",
|
||||
"模型排行": "Рейтинг моделей",
|
||||
"用户消耗排行": "Рейтинг потребления пользователей",
|
||||
"用户消耗趋势": "Тенденция потребления пользователей",
|
||||
"调试信息": "Отладочная информация",
|
||||
"谨慎": "Осторожно",
|
||||
"豆包": "Doubao",
|
||||
@@ -3147,7 +3166,7 @@
|
||||
"过期时间不能早于当前时间!": "Время истечения не может быть раньше текущего времени!",
|
||||
"过期时间快捷设置": "Быстрая настройка времени истечения",
|
||||
"过期时间格式错误!": "Ошибка формата времени истечения!",
|
||||
"运营设置": "Операционные настройки",
|
||||
"运营设置": "Операции",
|
||||
"运行中": "Running",
|
||||
"运行命令 (Command)": "Command",
|
||||
"运行时长": "Runtime Duration",
|
||||
@@ -3233,7 +3252,7 @@
|
||||
"通道 ${name} 余额更新成功!": "Баланс канала ${name} успешно обновлен!",
|
||||
"通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。": "Канал ${name} успешно протестирован, модель ${model} заняла ${time.toFixed(2)} секунд.",
|
||||
"通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Канал ${name} успешно протестирован, заняло ${time.toFixed(2)} секунд.",
|
||||
"速率限制设置": "Настройки ограничения скорости",
|
||||
"速率限制设置": "Ограничение скорости",
|
||||
"逻辑": "Логика",
|
||||
"邀请": "Приглашение",
|
||||
"邀请人": "Пригласивший",
|
||||
@@ -3396,6 +3415,7 @@
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Аудиовывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * аудио-коэффициент {{audioRatio}} * коэффициент аудиозавершения {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"页脚": "Подвал",
|
||||
"页面未找到,请检查您的浏览器地址是否正确": "Страница не найдена, пожалуйста, проверьте правильность адреса в браузере",
|
||||
"页面渲染出错,请刷新页面重试": "Произошла ошибка при отрисовке страницы. Пожалуйста, обновите страницу и попробуйте снова.",
|
||||
"顶栏管理": "Управление верхней панелью",
|
||||
"项": "элементов",
|
||||
"项目": "Проект",
|
||||
@@ -3462,140 +3482,6 @@
|
||||
"默认测试模型": "Модель для тестирования по умолчанию",
|
||||
"默认用户消息": "Здравствуйте",
|
||||
"默认补全倍率": "Коэффициент завершения по умолчанию",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Примечание: сопоставление эндпоинтов используется только для отображения в «Маркетплейсе моделей» и не влияет на реальный вызов. Чтобы настроить реальное поведение вызовов, перейдите в «Управление каналами».",
|
||||
"购买订阅获得模型额度/次数": "Купите подписку, чтобы получить лимит/количество использования моделей",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "RSA закрытый ключ Base64 (PKCS#8 DER) производственной среды",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "RSA закрытый ключ Base64 (PKCS#8 DER) песочницы",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "Открытый ключ Waffo Base64 (X.509 DER) производственной среды",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Открытый ключ Waffo Base64 (X.509 DER) песочницы",
|
||||
"支付方式类型": "Тип метода оплаты",
|
||||
"支付方式名称": "Название метода оплаты",
|
||||
"获取充值配置失败": "Не удалось получить конфигурацию пополнения",
|
||||
"获取充值配置异常": "Ошибка конфигурации пополнения",
|
||||
"分组相关设置": "Настройки, связанные с группами",
|
||||
"保存分组相关设置": "Сохранить настройки, связанные с группами",
|
||||
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "На этой странице показаны только модели без цены или базового коэффициента. После сохранения они будут автоматически удалены из списка.",
|
||||
"没有未设置定价的模型": "Нет моделей без цены",
|
||||
"当前没有未设置定价的模型": "Сейчас нет моделей без цены",
|
||||
"模型计费编辑器": "Редактор тарификации моделей",
|
||||
"价格摘要": "Сводка цен",
|
||||
"当前提示": "Текущие подсказки",
|
||||
"这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。": "В этом интерфейсе значения по умолчанию задаются через цены, а при сохранении они автоматически преобразуются в JSON коэффициентов, требуемый backend.",
|
||||
"当前未启用,需要时再打开即可。": "Это поле сейчас отключено. Включите его при необходимости.",
|
||||
"下面展示这个模型保存后会写入哪些后端字段,便于和原始 JSON 编辑框保持一致。": "Ниже показано, какие backend-поля будут записаны после сохранения, чтобы их было удобно сверять с редакторами исходного JSON.",
|
||||
"补全价格已锁定": "Цена завершения заблокирована",
|
||||
"后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "Фиксированный backend-коэффициент: {{ratio}}. Это поле только показывает вычисленную цену.",
|
||||
"这些价格都是可选项,不填也可以。": "Все эти цены необязательны и могут быть оставлены пустыми.",
|
||||
"请先开启并填写音频输入价格。": "Сначала включите и заполните цену аудио-ввода.",
|
||||
"输入模型名称,例如 gpt-4.1": "Введите имя модели, например gpt-4.1",
|
||||
"当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。": "У этой модели одновременно задана цена за запрос и конфигурация коэффициентов. При сохранении данные будут перезаписаны согласно текущему режиму тарификации.",
|
||||
"当前模型存在未显式设置输入倍率的扩展倍率;填写输入价格后会自动换算为价格字段。": "У этой модели есть дополнительные коэффициенты без явно заданного входного коэффициента; после ввода входной цены они будут автоматически преобразованы в ценовые поля.",
|
||||
"按量计费下需要先填写输入价格,才能保存其它价格项。": "При тарификации по объему сначала нужно указать входную цену, чтобы сохранить остальные ценовые поля.",
|
||||
"填写音频补全价格前,需要先填写音频输入价格。": "Перед указанием цены аудио-завершения сначала задайте цену аудио-ввода.",
|
||||
"模型 {{name}} 缺少输入价格,无法计算补全/缓存/图片/音频价格对应的倍率": "У модели {{name}} отсутствует входная цена, поэтому невозможно вычислить коэффициенты для завершения, кэша, изображений и аудио.",
|
||||
"模型 {{name}} 缺少音频输入价格,无法计算音频补全倍率": "У модели {{name}} отсутствует цена аудио-ввода, поэтому невозможно вычислить коэффициент аудио-завершения.",
|
||||
"批量应用当前模型价格": "Массово применить цену текущей модели",
|
||||
"请先选择一个作为模板的模型": "Сначала выберите модель-шаблон",
|
||||
"请先勾选需要批量设置的模型": "Сначала отметьте модели для массовой настройки",
|
||||
"已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型": "Ценовая конфигурация модели {{name}} массово применена к {{count}} моделям",
|
||||
"将把当前编辑中的模型 {{name}} 的价格配置,批量应用到已勾选的 {{count}} 个模型。": "Ценовая конфигурация редактируемой модели {{name}} будет применена к {{count}} выбранным моделям.",
|
||||
"适合同系列模型一起定价,例如把 gpt-5.1 的价格批量同步到 gpt-5.1-high、gpt-5.1-low 等模型。": "Подходит для совместной настройки цен вариантов одной модели, например синхронизации цены gpt-5.1 с gpt-5.1-high, gpt-5.1-low и похожими моделями.",
|
||||
"已勾选": "Выбрано",
|
||||
"当前编辑": "Текущее редактирование",
|
||||
"已勾选 {{count}} 个模型": "Выбрано моделей: {{count}}",
|
||||
"计费方式": "Режим тарификации",
|
||||
"未设置价格": "Цена не задана",
|
||||
"保存预览": "Предпросмотр сохранения",
|
||||
"基础价格": "Базовые цены",
|
||||
"扩展价格": "Дополнительные цены",
|
||||
"额外价格项": "Дополнительные ценовые позиции",
|
||||
"补全价格": "Цена завершения",
|
||||
"缓存读取价格": "Цена чтения входного кеша",
|
||||
"缓存创建价格": "Цена создания входного кеша",
|
||||
"图片输入价格": "Цена входного изображения",
|
||||
"音频输入价格": "Цена входного аудио",
|
||||
"音频补全价格": "Цена завершения аудио",
|
||||
"适合 MJ / 任务类等按次收费模型。": "Подходит для MJ и других моделей с тарификацией за запрос.",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "Коэффициент завершения для этой модели зафиксирован на уровне {{ratio}} на бэкенде. Цену завершения нельзя изменить здесь.",
|
||||
"计费显示模式": "Режим отображения тарификации",
|
||||
"价格模式(默认)": "Режим цен (по умолчанию)",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "Цена модели {{symbol}}{{price}} / запрос",
|
||||
"按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "За запрос {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "Цена модели: {{symbol}}{{price}} / запрос",
|
||||
"按次:{{symbol}}{{price}}": "За запрос: {{symbol}}{{price}}",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Фактическое списание: {{symbol}}{{total}} (включая групповую ценовую корректировку)",
|
||||
"缓存读取价格:{{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}}{{price}} / 1M tokens": "Цена создания кеша {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Цена создания кеша 5m: {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Цена создания кеша 5m {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Цена создания кеша 1h: {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Цена создания кеша 1h {{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}}{{price}} / 1M tokens",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "Цена входного аудио: {{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "Цена завершения аудио: {{symbol}}{{price}} / 1M tokens",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Web-поиск вызван {{webSearchCallCount}} раз",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "Поиск файлов вызван {{fileSearchCallCount}} раз",
|
||||
"图片倍率 {{imageRatio}}": "Коэффициент изображения {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "Аудио-коэффициент {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Обычный ввод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Кэшированный ввод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент кэша {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Ввод изображения: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент изображения {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Аудиоввод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * аудио-коэффициент {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Вывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент завершения {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web-поиск: {{count}} / 1K * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Поиск файлов: {{count}} / 1K * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Генерация изображения: 1 вызов * цена за единицу {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "Итого: {{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "Коэффициент модели {{modelRatio}}, коэффициент завершения {{completionRatio}}, аудио-коэффициент {{audioRatio}}, коэффициент аудиозавершения {{audioCompletionRatio}}, {{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Текстовый вывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент завершения {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Аудиовывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * аудио-коэффициент {{audioRatio}} * коэффициент аудиозавершения {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "Итого: текстовая часть {{textTotal}} + аудиочасть {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "Коэффициент модели {{modelRatio}}, коэффициент вывода {{completionRatio}}, коэффициент кэша {{cacheRatio}}, {{ratioType}} {{ratio}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Чтение кэша: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент кэша {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша 5m: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Создание кэша 1h: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент создания кэша 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Вывод: {{tokens}} / 1M * коэффициент модели {{modelRatio}} * коэффициент вывода {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "Пусто",
|
||||
"{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x",
|
||||
"模型价格:{{symbol}}{{price}}": "Цена модели: {{symbol}}{{price}}",
|
||||
"模型价格 {{price}}": "Цена модели {{price}}",
|
||||
"缓存读 {{price}} / 1M tokens": "Чтение кеша {{price}} / 1M tokens",
|
||||
"5m缓存创建 {{price}} / 1M tokens": "Создание кэша 5m {{price}} / 1M tokens",
|
||||
"1h缓存创建 {{price}} / 1M tokens": "Создание кэша 1h {{price}} / 1M tokens",
|
||||
"缓存创建 {{price}} / 1M tokens": "Создание кэша {{price}} / 1M tokens",
|
||||
"图片输入 {{price}} / 1M tokens": "Ввод изображения {{price}} / 1M tokens",
|
||||
"输入 {{price}} / 1M tokens": "Вход {{price}} / 1M tokens",
|
||||
"缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Кэш {{tokens}} токенов / 1M токенов * {{symbol}}{{price}}",
|
||||
"缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Создание кэша {{tokens}} токенов / 1M токенов * {{symbol}}{{price}}",
|
||||
"5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Создание кэша 5m {{tokens}} токенов / 1M токенов * {{symbol}}{{price}}",
|
||||
"1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Создание кэша 1h {{tokens}} токенов / 1M токенов * {{symbol}}{{price}}",
|
||||
"(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}": "(Ввод {{nonImageInput}} токенов + ввод изображения {{imageInput}} токенов / 1M токенов * {{symbol}}{{price}}",
|
||||
"图片输入价格:{{symbol}}{{total}} / 1M tokens": "Цена входного изображения: {{symbol}}{{total}} / 1M tokens",
|
||||
"文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Текстовый промпт {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + Текстовое дополнение {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + Аудио промпт {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + Аудио дополнение {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Цена модели {{symbol}}{{price}} / запрос * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"缓存读取价格:{{symbol}}{{total}} / 1M tokens": "Цена чтения кеша: {{symbol}}{{total}} / 1M tokens",
|
||||
"补全 {{completion}} tokens * 输出倍率 {{completionRatio}}": "Дополнение {{completion}} токенов * коэффициент вывода {{completionRatio}}",
|
||||
"补全倍率 {{completionRatio}}": "Коэффициент вывода {{completionRatio}}",
|
||||
"输入价格:{{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",
|
||||
"复制密钥": "Копировать ключ",
|
||||
"复制连接信息": "Копировать данные подключения",
|
||||
"检测到剪贴板中的连接信息": "В буфере обмена обнаружены данные подключения",
|
||||
"自动填入": "Заполнить",
|
||||
"忽略": "Игнорировать",
|
||||
"从剪贴板粘贴配置": "Вставить конфигурацию",
|
||||
"剪贴板中未检测到连接信息": "Данные подключения не найдены в буфере обмена",
|
||||
"连接信息已填入": "Данные подключения применены",
|
||||
"无法读取剪贴板": "Не удалось прочитать буфер обмена",
|
||||
"页面渲染出错,请刷新页面重试": "Произошла ошибка при отрисовке страницы. Пожалуйста, обновите страницу и попробуйте снова.",
|
||||
"刷新页面": "Обновить страницу",
|
||||
"(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(В настоящее время поддерживается только интерфейс YiPay, по умолчанию используется адрес сервера выше в качестве адреса обратного вызова!)",
|
||||
",当前无生效订阅,将自动使用钱包": ", нет активной подписки, автоматически будет использоваться кошелек.",
|
||||
",时间:": ", время: ",
|
||||
|
||||
Vendored
+36
-149
@@ -243,6 +243,7 @@
|
||||
"price_xxx 的商品价格 ID,新建产品后可获得": "ID giá sản phẩm cho price_xxx, có sẵn sau khi tạo sản phẩm mới",
|
||||
"safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "Trường safety_identifier giúp OpenAI xác định người dùng ứng dụng có thể vi phạm chính sách sử dụng. Tắt theo mặc định để bảo vệ quyền riêng tư của người dùng",
|
||||
"service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "Trường service_tier được sử dụng để chỉ định cấp độ dịch vụ. Cho phép truyền qua có thể dẫn đến việc tính phí thực tế cao hơn dự kiến. Tắt theo mặc định để tránh phí bổ sung",
|
||||
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Trường speed kiểm soát chế độ tốc độ suy luận Claude. Mặc định tắt để tránh vô tình chuyển sang chế độ fast",
|
||||
"sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "Khóa Stripe cho sk_xxx hoặc rk_xxx, thông tin nhạy cảm không được hiển thị",
|
||||
"standard 已被移除,vip 用户看不到": "standard has been removed, vip users cannot see it",
|
||||
"store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "Trường store ủy quyền cho OpenAI lưu trữ dữ liệu yêu cầu để đánh giá và tối ưu hóa sản phẩm. Tắt theo mặc định. Bật có thể khiến Codex hoạt động không chính xác",
|
||||
@@ -402,7 +403,7 @@
|
||||
"以下上游数据可能不可信:": "Dữ liệu thượng nguồn sau đây có thể không đáng tin cậy: ",
|
||||
"以下文件解析失败,已忽略:{{list}}": "Các tệp sau không phân tích được và đã bị bỏ qua: {{list}}",
|
||||
"以及": "và",
|
||||
"仪表盘设置": "Cài đặt bảng điều khiển",
|
||||
"仪表盘设置": "Bảng điều khiển",
|
||||
"价格": "Giá cả",
|
||||
"价格摘要": "Tóm tắt giá",
|
||||
"价格暂时不可用,请稍后重试": "Price temporarily unavailable, please try again later",
|
||||
@@ -432,11 +433,11 @@
|
||||
"余额充值管理": "Quản lý nạp tiền số dư",
|
||||
"作废": "Vô hiệu",
|
||||
"作废于": "Vô hiệu vào",
|
||||
"下一次重置": "Đặt lại tiếp theo",
|
||||
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "Sau khi vô hiệu, đăng ký sẽ mất hiệu lực ngay. Lịch sử không bị ảnh hưởng. Tiếp tục?",
|
||||
"作用域": "Phạm vi",
|
||||
"作用域:包含分组": "Phạm vi: Bao gồm nhóm",
|
||||
"作用域:包含模型名称": "Phạm vi: Bao gồm tên mô hình",
|
||||
"开启后,模型名称会参与 cache key(不同模型隔离)。": "Khi bật, tên mô hình sẽ được bao gồm trong cache key (cách ly các mô hình khác nhau).",
|
||||
"作用域:包含规则名称": "Phạm vi: Bao gồm tên quy tắc",
|
||||
"你似乎并没有修改什么": "Bạn dường như không sửa đổi gì cả",
|
||||
"你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "You can manually add them under “Custom model names”, click Fill and submit, or use the actions below to handle them automatically.",
|
||||
@@ -571,6 +572,7 @@
|
||||
"允许 inference_geo 透传": "Cho phép truyền inference_geo",
|
||||
"允许 safety_identifier 透传": "Cho phép safety_identifier truyền qua",
|
||||
"允许 service_tier 透传": "Cho phép service_tier truyền qua",
|
||||
"允许 speed 透传": "Cho phép truyền speed",
|
||||
"允许 stream_options.include_obfuscation 透传": "Cho phép truyền stream_options.include_obfuscation",
|
||||
"允许不安全的 Origin(HTTP)": "Cho phép Origin không an toàn (HTTP)",
|
||||
"允许回调(会泄露服务器 IP 地址)": "Cho phép gọi lại (sẽ làm lộ địa chỉ IP máy chủ)",
|
||||
@@ -667,7 +669,7 @@
|
||||
"其他": "Khác",
|
||||
"其他注册选项": "Tùy chọn đăng ký khác",
|
||||
"其他登录选项": "Tùy chọn đăng nhập khác",
|
||||
"其他设置": "Cài đặt khác",
|
||||
"其他设置": "Khác",
|
||||
"其他详情": "Other details",
|
||||
"内存 阈值 (%)": "Ngưỡng bộ nhớ (%)",
|
||||
"内存使用率超过此值时拒绝请求": "Từ chối yêu cầu khi sử dụng bộ nhớ vượt quá giá trị này",
|
||||
@@ -688,7 +690,7 @@
|
||||
"分类名称": "Tên danh mục",
|
||||
"分组": "Nhóm",
|
||||
"分组JSON设置": "Group JSON Settings",
|
||||
"分组与模型定价设置": "Cài đặt giá nhóm và mô hình",
|
||||
"分组与模型定价设置": "Nhóm & định giá mô hình",
|
||||
"分组价格": "Giá nhóm",
|
||||
"分组倍率": "Tỷ lệ nhóm",
|
||||
"分组倍率设置": "Cài đặt tỷ lệ nhóm",
|
||||
@@ -762,6 +764,7 @@
|
||||
"刷新统计": "Làm mới thống kê",
|
||||
"刷新缓存统计": "Làm mới thống kê bộ nhớ đệm",
|
||||
"刷新缓存统计失败": "Làm mới thống kê bộ nhớ đệm thất bại",
|
||||
"刷新页面": "Tải lại trang",
|
||||
"前往 io.net API Keys": "Go to io.net API Keys",
|
||||
"前往设置": "Go to Settings",
|
||||
"前往设置页面": "Go to Settings Page",
|
||||
@@ -813,6 +816,8 @@
|
||||
"原密码": "Mật khẩu cũ",
|
||||
"原生格式": "Định dạng gốc",
|
||||
"原生额度": "Hạn mức gốc",
|
||||
"使用原生额度输入": "Nhập hạn mức gốc",
|
||||
"收起原生额度输入": "Ẩn nhập hạn mức gốc",
|
||||
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "Hoàn tất loại bỏ trùng lặp: {{before}} khóa trước khi loại bỏ, {{after}} khóa sau khi loại bỏ",
|
||||
"参与官方同步": "Tham gia đồng bộ chính thức",
|
||||
"参数": "tham số",
|
||||
@@ -912,6 +917,7 @@
|
||||
"启用Gemini思考后缀适配": "Bật thích ứng hậu tố tư duy Gemini",
|
||||
"启用Ping间隔": "Bật khoảng thời gian Ping",
|
||||
"启用SMTP SSL": "Bật SMTP SSL",
|
||||
"强制使用 AUTH LOGIN": "Buộc AUTH LOGIN",
|
||||
"启用SSRF防护(推荐开启以保护服务器安全)": "Bật bảo vệ SSRF (Khuyên dùng để bảo mật máy chủ)",
|
||||
"启用供应商": "Bật nhà cung cấp",
|
||||
"启用全部": "Bật tất cả",
|
||||
@@ -1345,6 +1351,7 @@
|
||||
"开启后,将定期发送ping数据保持连接活跃": "Sau khi bật, dữ liệu ping sẽ được gửi định kỳ để giữ kết nối hoạt động",
|
||||
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "Sau khi bật, khi kênh nhóm hiện tại thất bại, nó sẽ thử kênh của nhóm tiếp theo theo thứ tự",
|
||||
"开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启": "Khi bật, tất cả các yêu cầu sẽ được chuyển tiếp trực tiếp đến thượng nguồn mà không cần xử lý (chuyển hướng và thích ứng kênh cũng sẽ bị vô hiệu hóa). Vui lòng bật một cách thận trọng.",
|
||||
"开启后,模型名称会参与 cache key(不同模型隔离)。": "Khi bật, tên mô hình sẽ được bao gồm trong cache key (cách ly các mô hình khác nhau).",
|
||||
"开启后,若该规则命中且请求失败,将不会切换渠道重试。": "Khi bật, nếu quy tắc này trúng và yêu cầu thất bại, sẽ không chuyển kênh để thử lại.",
|
||||
"开启后,规则名称会参与 cache key(不同规则隔离)。": "Khi bật, tên quy tắc sẽ tham gia vào cache key (cách ly theo quy tắc).",
|
||||
"开启后,该渠道请求 Claude 时将强制追加 ?beta=true(无需客户端手动传参)": "Khi bật, yêu cầu đến Claude qua kênh này sẽ tự động thêm ?beta=true (client không cần truyền thủ công)",
|
||||
@@ -1419,7 +1426,7 @@
|
||||
"思考预算占比": "Tỷ lệ ngân sách tư duy",
|
||||
"性能指标": "Chỉ số hiệu suất",
|
||||
"性能监控": "Giám sát hiệu suất",
|
||||
"性能设置": "Cài đặt hiệu suất",
|
||||
"性能设置": "Hiệu suất",
|
||||
"总 GPU 小时": "Total GPU Hours",
|
||||
"总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}": "Tổng giá: giá văn bản {{textPrice}} + giá âm thanh {{audioPrice}} = {{symbol}}{{total}}",
|
||||
"总分配内存": "Tổng bộ nhớ đã phân bổ",
|
||||
@@ -1568,7 +1575,7 @@
|
||||
"支付方式名称": "Tên phương thức thanh toán",
|
||||
"支付方式类型": "Loại phương thức thanh toán",
|
||||
"支付渠道": "Kênh thanh toán",
|
||||
"支付设置": "Cài đặt thanh toán",
|
||||
"支付设置": "Thanh toán",
|
||||
"支付请求失败": "Yêu cầu thanh toán thất bại",
|
||||
"支付金额": "Số tiền thanh toán",
|
||||
"支持 Ctrl+V 粘贴图片": "Hỗ trợ Ctrl+V để dán hình ảnh",
|
||||
@@ -1868,6 +1875,7 @@
|
||||
"条件规则": "Quy tắc điều kiện",
|
||||
"条件项设置": "Cài đặt mục điều kiện",
|
||||
"条日志已清理!": "nhật ký đã được xóa!",
|
||||
"条规则": "rules",
|
||||
"条,共": "của",
|
||||
"来源": "Nguồn",
|
||||
"来源于 IO.NET 部署": "From IO.NET Deployment",
|
||||
@@ -1962,6 +1970,7 @@
|
||||
"模型库": "Thư viện mô hình",
|
||||
"模型拉取失败: {{error}}": "Failed to pull model: {{error}}",
|
||||
"模型排序": "Sắp xếp mô hình",
|
||||
"模型排行": "Xếp hạng mô hình",
|
||||
"模型支持的接口端点信息": "Thông tin điểm cuối API được mô hình hỗ trợ",
|
||||
"模型数据分析": "Phân tích dữ liệu mô hình",
|
||||
"模型映射": "Ánh xạ mô hình",
|
||||
@@ -1978,7 +1987,7 @@
|
||||
"模型版本": "Phiên bản mô hình",
|
||||
"模型状态": "Trạng thái mô hình",
|
||||
"模型的详细描述和基本特性": "Mô tả chi tiết và các đặc điểm cơ bản của mô hình",
|
||||
"模型相关设置": "Cài đặt liên quan đến mô hình",
|
||||
"模型相关设置": "Mô hình liên quan",
|
||||
"模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "Cộng đồng mô hình cần sự đóng góp của mọi người. Nếu bạn phát hiện dữ liệu sai hoặc muốn đóng góp dữ liệu mô hình mới, vui lòng truy cập:",
|
||||
"模型管理": "Quản lý mô hình",
|
||||
"模型类型": "Loại mô hình",
|
||||
@@ -1995,7 +2004,7 @@
|
||||
"模型部署": "Model Deployment",
|
||||
"模型部署服务未启用": "Model deployment service is not enabled",
|
||||
"模型部署管理": "Model Deployment Management",
|
||||
"模型部署设置": "Model Deployment Settings",
|
||||
"模型部署设置": "Triển khai mô hình",
|
||||
"模型配置": "Cấu hình mô hình",
|
||||
"模型重定向": "Chuyển hướng mô hình",
|
||||
"模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:": "The following models from the redirect have not been added to the “Models” list and requests will fail due to no available model:",
|
||||
@@ -2184,6 +2193,7 @@
|
||||
"添加分类": "Thêm danh mục",
|
||||
"添加分组": "Thêm nhóm",
|
||||
"添加分组倍率": "Thêm tỷ lệ nhóm",
|
||||
"添加分组规则": "Add Group Rules",
|
||||
"添加后提交": "Submit after adding",
|
||||
"添加启动参数": "Add Startup Args",
|
||||
"添加启动命令": "Add Startup Command",
|
||||
@@ -2216,6 +2226,14 @@
|
||||
"添加键值对": "Thêm cặp khóa-giá trị",
|
||||
"添加问答": "Thêm hỏi đáp",
|
||||
"添加额度": "Thêm hạn ngạch",
|
||||
"减少": "Giảm",
|
||||
"覆盖": "Ghi đè",
|
||||
"调整额度": "Điều chỉnh hạn ngạch",
|
||||
"调整额度成功": "Điều chỉnh hạn ngạch thành công",
|
||||
"当前额度": "Hạn ngạch hiện tại",
|
||||
"变更": "Thay đổi",
|
||||
"预计结果": "Kết quả dự kiến",
|
||||
"正数为增加,负数为减少": "Số dương để tăng, số âm để giảm",
|
||||
"清理": "Dọn dẹp",
|
||||
"清理不活跃缓存": "Xóa cache không hoạt động",
|
||||
"清理历史日志": "Dọn dẹp nhật ký lịch sử",
|
||||
@@ -2413,6 +2431,8 @@
|
||||
"用户注册": "Đăng ký người dùng",
|
||||
"用户注册时看到的网站名称,比如'我的网站'": "Tên trang web người dùng nhìn thấy khi đăng ký, ví dụ: 'Trang web của tôi'",
|
||||
"用户注册设置": "Cài đặt đăng ký người dùng",
|
||||
"用户消耗排行": "Xếp hạng tiêu thụ người dùng",
|
||||
"用户消耗趋势": "Xu hướng tiêu thụ người dùng",
|
||||
"用户登录": "Đăng nhập người dùng",
|
||||
"用户的基本账户信息": "Thông tin tài khoản cơ bản của người dùng",
|
||||
"用户管理": "Quản lý người dùng",
|
||||
@@ -2555,6 +2575,7 @@
|
||||
"确认冲突项修改": "Xác nhận sửa đổi mục xung đột",
|
||||
"确认删除": "Xác nhận xóa",
|
||||
"确认删除模型": "Confirm Delete Model",
|
||||
"确认删除该分组的所有规则?": "Delete all rules for this group?",
|
||||
"确认删除该分组?": "Confirm delete this group?",
|
||||
"确认删除该规则?": "Confirm delete this rule?",
|
||||
"确认取消密码登录": "Xác nhận hủy đăng nhập mật khẩu",
|
||||
@@ -2756,7 +2777,7 @@
|
||||
"系统监控": "Giám sát hệ thống",
|
||||
"系统管理": "Quản lý hệ thống",
|
||||
"系统管理功能": "Chức năng quản lý hệ thống",
|
||||
"系统设置": "Cài đặt hệ thống",
|
||||
"系统设置": "Hệ thống",
|
||||
"系统访问令牌": "Mã thông báo truy cập hệ thống",
|
||||
"系统负载": "Tải hệ thống",
|
||||
"系统通知": "Thông báo hệ thống",
|
||||
@@ -2809,7 +2830,7 @@
|
||||
"绘图任务记录": "Hồ sơ tác vụ vẽ",
|
||||
"绘图日志": "Nhật ký vẽ",
|
||||
"绘图模型": "Mô hình vẽ",
|
||||
"绘图设置": "Cài đặt vẽ",
|
||||
"绘图设置": "Vẽ",
|
||||
"统一的": "Cổng thống nhất",
|
||||
"统计": "Thống kê",
|
||||
"统计Tokens": "Thống kê Tokens",
|
||||
@@ -2900,7 +2921,7 @@
|
||||
"聊天区域": "Khu vực trò chuyện",
|
||||
"聊天应用名称": "Tên ứng dụng trò chuyện",
|
||||
"聊天应用名称已存在,请使用其他名称": "Tên ứng dụng trò chuyện đã tồn tại, vui lòng sử dụng tên khác",
|
||||
"聊天设置": "Cài đặt trò chuyện",
|
||||
"聊天设置": "Trò chuyện",
|
||||
"聊天配置": "Cấu hình trò chuyện",
|
||||
"聊天链接配置错误,请联系管理员": "Lỗi cấu hình liên kết trò chuyện, vui lòng liên hệ quản trị viên",
|
||||
"联系": "Liên hệ",
|
||||
@@ -3225,6 +3246,7 @@
|
||||
"请求参数无效": "Invalid request parameters",
|
||||
"请求发生错误": "Đã xảy ra lỗi yêu cầu",
|
||||
"请求发生错误: ": "Đã xảy ra lỗi yêu cầu: ",
|
||||
"模型价格未配置": "Giá mô hình chưa được cấu hình",
|
||||
"请求后端接口失败:": "Yêu cầu giao diện phụ trợ thất bại: ",
|
||||
"请求失败": "Yêu cầu thất bại",
|
||||
"请求失败,请重试": "Yêu cầu thất bại, vui lòng thử lại",
|
||||
@@ -3473,9 +3495,6 @@
|
||||
"调用次数分布": "Phân phối số lần gọi",
|
||||
"调用次数排行": "Xếp hạng số lần gọi",
|
||||
"调用趋势": "Xu hướng cuộc gọi",
|
||||
"模型排行": "Xếp hạng mô hình",
|
||||
"用户消耗排行": "Xếp hạng tiêu thụ người dùng",
|
||||
"用户消耗趋势": "Xu hướng tiêu thụ người dùng",
|
||||
"调试信息": "Thông tin gỡ lỗi",
|
||||
"谨慎": "Thận trọng",
|
||||
"豆包": "Doubao",
|
||||
@@ -3592,7 +3611,7 @@
|
||||
"过期时间不能早于当前时间!": "Thời gian hết hạn không thể sớm hơn thời gian hiện tại!",
|
||||
"过期时间快捷设置": "Cài đặt nhanh thời gian hết hạn",
|
||||
"过期时间格式错误!": "Lỗi định dạng thời gian hết hạn!",
|
||||
"运营设置": "Cài đặt vận hành",
|
||||
"运营设置": "Vận hành",
|
||||
"运行中": "Đang chạy",
|
||||
"运行命令 (Command)": "Command",
|
||||
"运行时长": "Runtime Duration",
|
||||
@@ -3716,7 +3735,7 @@
|
||||
"通道管理": "Quản lý kênh",
|
||||
"通道类型": "Loại kênh",
|
||||
"通道设置": "Cài đặt kênh",
|
||||
"速率限制设置": "Cài đặt giới hạn tốc độ",
|
||||
"速率限制设置": "Giới hạn tốc độ",
|
||||
"逻辑": "Logic",
|
||||
"邀请": "Mời",
|
||||
"邀请人": "Người mời",
|
||||
@@ -3931,6 +3950,7 @@
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra âm thanh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số âm thanh {{audioRatio}} * hệ số hoàn thành âm thanh {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"页脚": "Chân trang",
|
||||
"页面未找到,请检查您的浏览器地址是否正确": "Không tìm thấy trang, vui lòng kiểm tra xem địa chỉ trình duyệt của bạn có chính xác không",
|
||||
"页面渲染出错,请刷新页面重试": "Đã xảy ra lỗi khi hiển thị trang. Vui lòng tải lại trang và thử lại.",
|
||||
"顶栏管理": "Quản lý thanh tiêu đề",
|
||||
"项": "mục",
|
||||
"项目": "Dự án",
|
||||
@@ -3997,139 +4017,6 @@
|
||||
"默认测试模型": "Mô hình kiểm tra mặc định",
|
||||
"默认用户消息": "Xin chào",
|
||||
"默认补全倍率": "Tỷ lệ hoàn thành mặc định",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "Lưu ý: Ánh xạ endpoint chỉ dùng để hiển thị trong \"Chợ mô hình\" và không ảnh hưởng đến việc gọi thực tế. Để cấu hình gọi thực tế, vui lòng vào \"Quản lý kênh\".",
|
||||
"购买订阅获得模型额度/次数": "Mua đăng ký để nhận hạn mức/lượt dùng mô hình",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "Khóa riêng RSA Base64 (PKCS#8 DER) môi trường sản xuất",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "Khóa riêng RSA Base64 (PKCS#8 DER) môi trường sandbox",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "Khóa công khai Waffo Base64 (X.509 DER) môi trường sản xuất",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "Khóa công khai Waffo Base64 (X.509 DER) môi trường sandbox",
|
||||
"支付方式类型": "Loại phương thức thanh toán",
|
||||
"支付方式名称": "Tên phương thức thanh toán",
|
||||
"获取充值配置失败": "Không thể lấy cấu hình nạp tiền",
|
||||
"获取充值配置异常": "Lỗi cấu hình nạp tiền",
|
||||
"分组相关设置": "Cài đặt liên quan đến nhóm",
|
||||
"保存分组相关设置": "Lưu cài đặt liên quan đến nhóm",
|
||||
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "Trang này chỉ hiển thị các mô hình chưa thiết lập giá hoặc tỷ lệ cơ bản. Sau khi lưu, chúng sẽ tự động biến mất khỏi danh sách.",
|
||||
"没有未设置定价的模型": "Không có mô hình chưa thiết lập giá",
|
||||
"当前没有未设置定价的模型": "Hiện không có mô hình nào chưa thiết lập giá",
|
||||
"模型计费编辑器": "Trình chỉnh sửa giá mô hình",
|
||||
"价格摘要": "Tóm tắt giá",
|
||||
"当前提示": "Gợi ý hiện tại",
|
||||
"这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。": "Giao diện này mặc định nhập theo giá, khi lưu sẽ tự động quy đổi lại thành JSON tỷ lệ mà backend yêu cầu.",
|
||||
"当前未启用,需要时再打开即可。": "Trường này hiện đang tắt. Hãy bật khi cần.",
|
||||
"下面展示这个模型保存后会写入哪些后端字段,便于和原始 JSON 编辑框保持一致。": "Bên dưới hiển thị các trường backend sẽ được ghi sau khi lưu, giúp bạn dễ đối chiếu với ô chỉnh sửa JSON gốc.",
|
||||
"补全价格已锁定": "Giá hoàn thành đã bị khóa",
|
||||
"后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "Tỷ lệ cố định từ backend: {{ratio}}. Trường này chỉ hiển thị giá sau khi quy đổi.",
|
||||
"这些价格都是可选项,不填也可以。": "Tất cả các mức giá này đều là tùy chọn và có thể để trống.",
|
||||
"请先开启并填写音频输入价格。": "Hãy bật và điền giá đầu vào âm thanh trước.",
|
||||
"输入模型名称,例如 gpt-4.1": "Nhập tên mô hình, ví dụ gpt-4.1",
|
||||
"当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。": "Mô hình này hiện đồng thời có giá theo lượt gọi và cấu hình tỷ lệ. Khi lưu, dữ liệu sẽ bị ghi đè theo chế độ tính phí hiện tại.",
|
||||
"当前模型存在未显式设置输入倍率的扩展倍率;填写输入价格后会自动换算为价格字段。": "Mô hình này có các tỷ lệ mở rộng mà chưa đặt rõ tỷ lệ đầu vào; sau khi điền giá đầu vào, chúng sẽ tự động được quy đổi thành trường giá.",
|
||||
"按量计费下需要先填写输入价格,才能保存其它价格项。": "Ở chế độ tính phí theo lượng, cần điền giá đầu vào trước thì mới lưu được các mục giá khác.",
|
||||
"填写音频补全价格前,需要先填写音频输入价格。": "Trước khi nhập giá hoàn thành âm thanh, hãy nhập giá đầu vào âm thanh trước.",
|
||||
"模型 {{name}} 缺少输入价格,无法计算补全/缓存/图片/音频价格对应的倍率": "Mô hình {{name}} thiếu giá đầu vào, nên không thể tính tỷ lệ tương ứng cho giá hoàn thành, bộ nhớ đệm, hình ảnh và âm thanh.",
|
||||
"模型 {{name}} 缺少音频输入价格,无法计算音频补全倍率": "Mô hình {{name}} thiếu giá đầu vào âm thanh, nên không thể tính tỷ lệ hoàn thành âm thanh.",
|
||||
"批量应用当前模型价格": "Áp dụng hàng loạt giá của mô hình hiện tại",
|
||||
"请先选择一个作为模板的模型": "Vui lòng chọn trước một mô hình làm mẫu",
|
||||
"请先勾选需要批量设置的模型": "Vui lòng chọn các mô hình cần thiết lập hàng loạt trước",
|
||||
"已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型": "Đã áp dụng hàng loạt cấu hình giá của mô hình {{name}} cho {{count}} mô hình",
|
||||
"将把当前编辑中的模型 {{name}} 的价格配置,批量应用到已勾选的 {{count}} 个模型。": "Cấu hình giá của mô hình đang chỉnh sửa {{name}} sẽ được áp dụng hàng loạt cho {{count}} mô hình đã chọn.",
|
||||
"适合同系列模型一起定价,例如把 gpt-5.1 的价格批量同步到 gpt-5.1-high、gpt-5.1-low 等模型。": "Phù hợp để định giá cùng lúc các biến thể cùng dòng, ví dụ đồng bộ giá của gpt-5.1 sang gpt-5.1-high, gpt-5.1-low và các mô hình tương tự.",
|
||||
"已勾选": "Đã chọn",
|
||||
"当前编辑": "Đang chỉnh sửa",
|
||||
"已勾选 {{count}} 个模型": "Đã chọn {{count}} mô hình",
|
||||
"计费方式": "Chế độ tính phí",
|
||||
"未设置价格": "Chưa thiết lập giá",
|
||||
"保存预览": "Xem trước khi lưu",
|
||||
"基础价格": "Giá cơ bản",
|
||||
"扩展价格": "Giá mở rộng",
|
||||
"额外价格项": "Mục giá bổ sung",
|
||||
"补全价格": "Giá hoàn thành",
|
||||
"缓存读取价格": "Giá đọc bộ nhớ đệm đầu vào",
|
||||
"缓存创建价格": "Giá tạo bộ nhớ đệm đầu vào",
|
||||
"图片输入价格": "Giá đầu vào hình ảnh",
|
||||
"音频输入价格": "Giá đầu vào âm thanh",
|
||||
"音频补全价格": "Giá hoàn thành âm thanh",
|
||||
"适合 MJ / 任务类等按次收费模型。": "Phù hợp cho MJ và các mô hình tính phí theo lượt gọi tương tự.",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "Tỷ lệ hoàn thành của mô hình này được backend cố định ở {{ratio}}. Không thể chỉnh giá hoàn thành tại đây.",
|
||||
"计费显示模式": "Chế độ hiển thị tính phí",
|
||||
"价格模式(默认)": "Chế độ giá (mặc định)",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "Giá mô hình {{symbol}}{{price}} / lượt gọi",
|
||||
"按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Theo lượt gọi {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "Giá mô hình: {{symbol}}{{price}} / lượt gọi",
|
||||
"按次:{{symbol}}{{price}}": "Theo lượt gọi: {{symbol}}{{price}}",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "Khoản phí thực tế: {{symbol}}{{total}} (đã bao gồm điều chỉnh giá theo nhóm)",
|
||||
"缓存读取价格:{{symbol}}{{price}} / 1M tokens": "Giá đọc bộ nhớ đệm: {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存读取价格 {{symbol}}{{price}} / 1M tokens": "Giá đọc bộ nhớ đệm {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm: {{symbol}}{{price}} / 1M tokens",
|
||||
"缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 5m: {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 5m {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 1h: {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "Giá tạo bộ nhớ đệm 1h {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格:{{symbol}}{{price}} / 1M tokens": "Giá đầu vào hình ảnh: {{symbol}}{{price}} / 1M tokens",
|
||||
"图片输入价格 {{symbol}}{{price}} / 1M tokens": "Giá đầu vào hình ảnh {{symbol}}{{price}} / 1M tokens",
|
||||
"输入价格 {{symbol}}{{price}} / 1M tokens": "Giá đầu vào {{symbol}}{{price}} / 1M tokens",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "Giá đầu vào âm thanh: {{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "Giá hoàn thành âm thanh: {{symbol}}{{price}} / 1M tokens",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Đã gọi tìm kiếm Web {{webSearchCallCount}} lần",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "Đã gọi tìm kiếm tệp {{fileSearchCallCount}} lần",
|
||||
"图片倍率 {{imageRatio}}": "Hệ số hình ảnh {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "Hệ số âm thanh {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào thường: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào bộ nhớ đệm: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số bộ nhớ đệm {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào hình ảnh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số hình ảnh {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu vào âm thanh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số âm thanh {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số hoàn thành {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tìm kiếm Web: {{count}} / 1K * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tìm kiếm tệp: {{count}} / 1K * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo ảnh: 1 lần gọi * đơn giá {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "Tổng cộng: {{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "Hệ số mô hình {{modelRatio}}, hệ số hoàn thành {{completionRatio}}, hệ số âm thanh {{audioRatio}}, hệ số hoàn thành âm thanh {{audioCompletionRatio}}, {{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra văn bản: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số hoàn thành {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra âm thanh: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số âm thanh {{audioRatio}} * hệ số hoàn thành âm thanh {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "Tổng cộng: phần văn bản {{textTotal}} + phần âm thanh {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "Hệ số mô hình {{modelRatio}}, hệ số đầu ra {{completionRatio}}, hệ số bộ nhớ đệm {{cacheRatio}}, {{ratioType}} {{ratio}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đọc bộ nhớ đệm: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số bộ nhớ đệm {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm 5m: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm 5m {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "Tạo bộ nhớ đệm 1h: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số tạo bộ nhớ đệm 1h {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "Đầu ra: {{tokens}} / 1M * hệ số mô hình {{modelRatio}} * hệ số đầu ra {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "Trống",
|
||||
"{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x",
|
||||
"模型价格:{{symbol}}{{price}}": "Giá mô hình: {{symbol}}{{price}}",
|
||||
"模型价格 {{price}}": "Giá mô hình {{price}}",
|
||||
"缓存读 {{price}} / 1M tokens": "Đọc bộ nhớ đệm {{price}} / 1M tokens",
|
||||
"5m缓存创建 {{price}} / 1M tokens": "Tạo bộ nhớ đệm 5m {{price}} / 1M tokens",
|
||||
"1h缓存创建 {{price}} / 1M tokens": "Tạo bộ nhớ đệm 1h {{price}} / 1M tokens",
|
||||
"缓存创建 {{price}} / 1M tokens": "Tạo bộ nhớ đệm {{price}} / 1M tokens",
|
||||
"图片输入 {{price}} / 1M tokens": "Đầu vào hình ảnh {{price}} / 1M tokens",
|
||||
"输入 {{price}} / 1M tokens": "Đầu vào {{price}} / 1M tokens",
|
||||
"缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Bộ nhớ đệm {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Tạo bộ nhớ đệm {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Tạo bộ nhớ đệm 5m {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "Tạo bộ nhớ đệm 1h {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}": "(Đầu vào {{nonImageInput}} tokens + Đầu vào hình ảnh {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"图片输入价格:{{symbol}}{{total}} / 1M tokens": "Giá đầu vào hình ảnh: {{symbol}}{{total}} / 1M tokens",
|
||||
"文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Prompt văn bản {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + Hoàn thành văn bản {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + Prompt âm thanh {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + Hoàn thành âm thanh {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "Giá mô hình {{symbol}}{{price}} / lượt gọi * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"缓存读取价格:{{symbol}}{{total}} / 1M tokens": "Giá đọc bộ nhớ đệm: {{symbol}}{{total}} / 1M tokens",
|
||||
"补全 {{completion}} tokens * 输出倍率 {{completionRatio}}": "Hoàn thành {{completion}} tokens * Tỷ lệ đầu ra {{completionRatio}}",
|
||||
"补全倍率 {{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",
|
||||
"复制密钥": "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",
|
||||
"页面渲染出错,请刷新页面重试": "Đã xảy ra lỗi khi hiển thị trang. Vui lòng tải lại trang và thử lại.",
|
||||
"刷新页面": "Tải lại trang",
|
||||
"(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Hiện tại chỉ hỗ trợ giao diện Epay, địa chỉ máy chủ phía trên được sử dụng làm địa chỉ gọi lại theo mặc định!)",
|
||||
",当前无生效订阅,将自动使用钱包": ", hiện không có gói đăng ký hiệu lực, sẽ tự động dùng ví.",
|
||||
",时间:": ", thời gian:",
|
||||
|
||||
Vendored
+16
-1
@@ -140,6 +140,7 @@
|
||||
"Reasoning Effort": "Reasoning Effort",
|
||||
"safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私",
|
||||
"service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用",
|
||||
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式",
|
||||
"sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示",
|
||||
"SMTP 发送者邮箱": "SMTP 发送者邮箱",
|
||||
"SMTP 服务器地址": "SMTP 服务器地址",
|
||||
@@ -286,7 +287,7 @@
|
||||
"以下上游数据可能不可信:": "以下上游数据可能不可信:",
|
||||
"以下文件解析失败,已忽略:{{list}}": "以下文件解析失败,已忽略:{{list}}",
|
||||
"以及": "以及",
|
||||
"仪表盘设置": "仪表盘设置",
|
||||
"仪表盘设置": "仪表盘",
|
||||
"价格": "价格",
|
||||
"价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}": "价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}",
|
||||
"价格:${{price}} * {{ratioType}}:{{ratio}}": "价格:${{price}} * {{ratioType}}:{{ratio}}",
|
||||
@@ -410,6 +411,7 @@
|
||||
"允许 HTTP 协议图片请求(适用于自部署代理)": "允许 HTTP 协议图片请求(适用于自部署代理)",
|
||||
"允许 safety_identifier 透传": "允许 safety_identifier 透传",
|
||||
"允许 service_tier 透传": "允许 service_tier 透传",
|
||||
"允许 speed 透传": "允许 speed 透传",
|
||||
"允许 Turnstile 用户校验": "允许 Turnstile 用户校验",
|
||||
"允许不安全的 Origin(HTTP)": "允许不安全的 Origin(HTTP)",
|
||||
"允许回调(会泄露服务器 IP 地址)": "允许回调(会泄露服务器 IP 地址)",
|
||||
@@ -680,6 +682,7 @@
|
||||
"启用Gemini思考后缀适配": "启用Gemini思考后缀适配",
|
||||
"启用Ping间隔": "启用Ping间隔",
|
||||
"启用SMTP SSL": "启用SMTP SSL",
|
||||
"强制使用 AUTH LOGIN": "强制使用 AUTH LOGIN",
|
||||
"启用SSRF防护(推荐开启以保护服务器安全)": "启用SSRF防护(推荐开启以保护服务器安全)",
|
||||
"启用全部": "启用全部",
|
||||
"启用后可接入 io.net GPU 资源": "启用后可接入 io.net GPU 资源",
|
||||
@@ -1604,6 +1607,14 @@
|
||||
"添加键值对": "添加键值对",
|
||||
"添加问答": "添加问答",
|
||||
"添加额度": "添加额度",
|
||||
"减少": "减少",
|
||||
"覆盖": "覆盖",
|
||||
"调整额度": "调整额度",
|
||||
"调整额度成功": "调整额度成功",
|
||||
"当前额度": "当前额度",
|
||||
"变更": "变更",
|
||||
"预计结果": "预计结果",
|
||||
"正数为增加,负数为减少": "正数为增加,负数为减少",
|
||||
"清理方式": "清理方式",
|
||||
"清理日志文件": "清理日志文件",
|
||||
"清空": "清空",
|
||||
@@ -2144,6 +2155,7 @@
|
||||
"请求参数无效": "请求参数无效",
|
||||
"请求发生错误": "请求发生错误",
|
||||
"请求发生错误: ": "请求发生错误: ",
|
||||
"模型价格未配置": "模型价格未配置",
|
||||
"请求后端接口失败:": "请求后端接口失败:",
|
||||
"请求失败": "请求失败",
|
||||
"请求头覆盖": "请求头覆盖",
|
||||
@@ -2736,6 +2748,8 @@
|
||||
"请输入总额度": "请输入总额度",
|
||||
"0 表示不限": "0 表示不限",
|
||||
"原生额度": "原生额度",
|
||||
"使用原生额度输入": "使用原生额度输入",
|
||||
"收起原生额度输入": "收起原生额度输入",
|
||||
"升级分组": "升级分组",
|
||||
"不升级": "不升级",
|
||||
"购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。": "购买或手动新增订阅会升级到该分组;当套餐失效/过期或手动作废/删除后,将回退到升级前分组。回退不会立即生效,通常会有几分钟延迟。",
|
||||
@@ -2785,6 +2799,7 @@
|
||||
"至": "至",
|
||||
"过期于": "过期于",
|
||||
"作废于": "作废于",
|
||||
"下一次重置": "下一次重置",
|
||||
"购买套餐后即可享受模型权益": "购买套餐后即可享受模型权益",
|
||||
"限购": "限购",
|
||||
"推荐": "推荐",
|
||||
|
||||
Vendored
+27
-218
@@ -198,9 +198,11 @@
|
||||
"default为默认设置,可单独设置每个分类的安全等级": "default為預設設定,可單獨設定每個分類的安全等級",
|
||||
"default为默认设置,可单独设置每个模型的版本": "default為預設設定,可單獨設定每個模型的版本",
|
||||
"false": "false",
|
||||
"inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息": "inference_geo 字段用於控制 Claude 資料駐留推理區域。預設關閉以避免未經授權透傳地域資訊",
|
||||
"price_xxx 的商品价格 ID,新建产品后可获得": "price_xxx 的商品價格 ID,新建產品後可獲得",
|
||||
"safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私": "safety_identifier 字段用於幫助 OpenAI 識別可能違反使用政策的應用程式使用者。預設關閉以保護使用者隱私",
|
||||
"service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用": "service_tier 字段用於指定服務層級,允許透傳可能導致實際計費高於預期。預設關閉以避免額外費用",
|
||||
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "speed 字段用於控制 Claude 推理速度模式。預設關閉以避免意外切換到 fast 模式",
|
||||
"sk_xxx 或 rk_xxx 的 Stripe 密钥,敏感信息不显示": "sk_xxx 或 rk_xxx 的 Stripe 密鑰,敏感資訊不顯示",
|
||||
"standard 已被移除,vip 用户看不到": "standard 已被移除,vip 使用者看不到",
|
||||
"store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "store 字段用於授權 OpenAI 存儲請求數據以評估和優化產品。預設關閉,開啟後可能導致 Codex 無法正常使用",
|
||||
@@ -379,6 +381,7 @@
|
||||
"余额充值管理": "餘額儲值管理",
|
||||
"作废": "作廢",
|
||||
"作废于": "作廢於",
|
||||
"下一次重置": "下一次重置",
|
||||
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "作廢後該訂閱將立即失效,歷史記錄不受影響。是否繼續?",
|
||||
"你似乎并没有修改什么": "你似乎並沒有修改什麼",
|
||||
"你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "你可以在「自訂模型名稱」處手動添加它們,然後點擊填入後再提交,或者直接使用下方操作自動處理。",
|
||||
@@ -449,7 +452,7 @@
|
||||
"保存 Turnstile 设置": "儲存 Turnstile 設定",
|
||||
"保存 WeChat Server 设置": "儲存 WeChat Server 設定",
|
||||
"保存分组倍率设置": "儲存分組倍率設定",
|
||||
"保存分组相关设置": "儲存分組相關設定",
|
||||
"保存分组相关设置": "保存分組相關設定",
|
||||
"保存备用码": "儲存備用碼",
|
||||
"保存备用码以备不时之需": "儲存備用碼以備不時之需",
|
||||
"保存失败": "儲存失敗",
|
||||
@@ -497,6 +500,8 @@
|
||||
"允许 Turnstile 用户校验": "允許 Turnstile 使用者校驗",
|
||||
"允许 safety_identifier 透传": "允許 safety_identifier 透傳",
|
||||
"允许 service_tier 透传": "允許 service_tier 透傳",
|
||||
"允许 inference_geo 透传": "允許 inference_geo 透傳",
|
||||
"允许 speed 透传": "允許 speed 透傳",
|
||||
"允许不安全的 Origin(HTTP)": "允許不安全的 Origin(HTTP)",
|
||||
"允许回调(会泄露服务器 IP 地址)": "允許回調(會洩露伺服器 IP 位址)",
|
||||
"允许在 Stripe 支付中输入促销码": "允許在 Stripe 支付中輸入促銷碼",
|
||||
@@ -602,7 +607,7 @@
|
||||
"分类名称": "分類名稱",
|
||||
"分组": "分組",
|
||||
"分组JSON设置": "分組 JSON 設定",
|
||||
"分组与模型定价设置": "分組與模型定價設定",
|
||||
"分组与模型定价设置": "分組與模型定價",
|
||||
"分组价格": "分組價格",
|
||||
"分组倍率": "分組倍率",
|
||||
"分组倍率设置": "分組倍率設定",
|
||||
@@ -670,6 +675,7 @@
|
||||
"刷新容器信息": "刷新容器資訊",
|
||||
"刷新日志": "刷新日誌",
|
||||
"刷新统计": "刷新統計",
|
||||
"刷新页面": "重新整理頁面",
|
||||
"前往 io.net API Keys": "前往 io.net API Keys",
|
||||
"前往设置": "前往設定",
|
||||
"前往设置页面": "前往設定頁面",
|
||||
@@ -718,6 +724,8 @@
|
||||
"原密码": "原密碼",
|
||||
"原生格式": "原生格式",
|
||||
"原生额度": "原生額度",
|
||||
"使用原生额度输入": "使用原生額度輸入",
|
||||
"收起原生额度输入": "收起原生額度輸入",
|
||||
"去重完成:去重前 {{before}} 个密钥,去重后 {{after}} 个密钥": "去重完成:去重前 {{before}} 個密鑰,去重後 {{after}} 個密鑰",
|
||||
"参与官方同步": "參與官方同步",
|
||||
"参数": "參數",
|
||||
@@ -797,6 +805,7 @@
|
||||
"启用Gemini思考后缀适配": "啟用Gemini思考後綴相容",
|
||||
"启用Ping间隔": "啟用Ping間隔",
|
||||
"启用SMTP SSL": "啟用SMTP SSL",
|
||||
"强制使用 AUTH LOGIN": "強制使用 AUTH LOGIN",
|
||||
"启用SSRF防护(推荐开启以保护服务器安全)": "啟用SSRF防護(推薦開啟以保護伺服器安全)",
|
||||
"启用全部": "啟用全部",
|
||||
"启用后可接入 io.net GPU 资源": "啟用後可接入 io.net GPU 資源",
|
||||
@@ -1657,6 +1666,7 @@
|
||||
"条": "條",
|
||||
"条 - 第": "條 - 第",
|
||||
"条日志已清理!": "條日誌已清理!",
|
||||
"条规则": "條規則",
|
||||
"条,共": "條,共",
|
||||
"来源": "來源",
|
||||
"来源于 IO.NET 部署": "來源於 IO.NET 部署",
|
||||
@@ -1743,6 +1753,7 @@
|
||||
"模型定价,需要登录访问": "模型定價,需要登錄訪問",
|
||||
"模型广场": "模型廣場",
|
||||
"模型拉取失败: {{error}}": "模型拉取失敗: {{error}}",
|
||||
"模型排行": "模型排行",
|
||||
"模型支持的接口端点信息": "模型支援的接口端點資訊",
|
||||
"模型数据分析": "模型數據分析",
|
||||
"模型映射必须是合法的 JSON 格式!": "模型映射必須是合法的 JSON 格式!",
|
||||
@@ -1884,6 +1895,7 @@
|
||||
"添加公告": "添加公告",
|
||||
"添加分类": "添加分類",
|
||||
"添加分组": "新增分組",
|
||||
"添加分组规则": "新增分組規則",
|
||||
"添加后提交": "添加後提交",
|
||||
"添加启动参数": "添加啟動參數",
|
||||
"添加启动命令": "添加啟動命令",
|
||||
@@ -1900,6 +1912,14 @@
|
||||
"添加键值对": "添加鍵值對",
|
||||
"添加问答": "添加問答",
|
||||
"添加额度": "添加額度",
|
||||
"减少": "減少",
|
||||
"覆盖": "覆蓋",
|
||||
"调整额度": "調整額度",
|
||||
"调整额度成功": "調整額度成功",
|
||||
"当前额度": "當前額度",
|
||||
"变更": "變更",
|
||||
"预计结果": "預計結果",
|
||||
"正数为增加,负数为减少": "正數為增加,負數為減少",
|
||||
"清理不活跃缓存": "清理不活躍快取",
|
||||
"清理失败": "清理失敗",
|
||||
"清理方式": "清理方式",
|
||||
@@ -2006,6 +2026,8 @@
|
||||
"用户每周期最多请求完成次数": "使用者每週期最多請求完成次數",
|
||||
"用户每周期最多请求次数": "使用者每週期最多請求次數",
|
||||
"用户注册时看到的网站名称,比如'我的网站'": "使用者註冊時看到的網站名稱,比如'我的網站'",
|
||||
"用户消耗排行": "用戶消耗排行",
|
||||
"用户消耗趋势": "用戶消耗趨勢",
|
||||
"用户的基本账户信息": "使用者的基本帳號資訊",
|
||||
"用户管理": "使用者管理",
|
||||
"用户组": "使用者組",
|
||||
@@ -2089,6 +2111,7 @@
|
||||
"确认冲突项修改": "確認衝突項修改",
|
||||
"确认删除": "確認刪除",
|
||||
"确认删除模型": "確認刪除模型",
|
||||
"确认删除该分组的所有规则?": "確認刪除該分組的所有規則?",
|
||||
"确认删除该分组?": "確認刪除該分組?",
|
||||
"确认删除该规则?": "確認刪除該規則?",
|
||||
"确认取消密码登录": "確認取消密碼登錄",
|
||||
@@ -2545,6 +2568,7 @@
|
||||
"请求参数无效": "請求參數無效",
|
||||
"请求发生错误": "請求發生錯誤",
|
||||
"请求发生错误: ": "請求發生錯誤: ",
|
||||
"模型价格未配置": "模型價格未配置",
|
||||
"请求后端接口失败:": "請求後端接口失敗:",
|
||||
"请求失败": "請求失敗",
|
||||
"请求头覆盖": "請求頭覆蓋",
|
||||
@@ -2720,9 +2744,6 @@
|
||||
"调用次数分布": "調用次數分佈",
|
||||
"调用次数排行": "調用次數排行",
|
||||
"调用趋势": "調用趨勢",
|
||||
"模型排行": "模型排行",
|
||||
"用户消耗排行": "用戶消耗排行",
|
||||
"用户消耗趋势": "用戶消耗趨勢",
|
||||
"调试信息": "除錯訊息",
|
||||
"谨慎": "謹慎",
|
||||
"豆包": "豆包",
|
||||
@@ -3045,6 +3066,7 @@
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音訊輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音訊倍率 {{audioRatio}} * 音訊補全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"页脚": "頁腳",
|
||||
"页面未找到,请检查您的浏览器地址是否正确": "頁面未找到,請檢查您的瀏覽器位址是否正確",
|
||||
"页面渲染出错,请刷新页面重试": "頁面渲染出錯,請重新整理頁面重試",
|
||||
"顶栏管理": "頂欄管理",
|
||||
"项目": "項目",
|
||||
"项目内容": "項目內容",
|
||||
@@ -3114,219 +3136,6 @@
|
||||
"默认补全倍率": "預設補全倍率",
|
||||
"(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(當前僅支援易支付接口,預設使用上方伺服器位址作為回調位址!)",
|
||||
",当前无生效订阅,将自动使用钱包": ",當前無生效訂閱,將自動使用錢包",
|
||||
"个已过期": "個已過期",
|
||||
"订阅": "訂閱",
|
||||
"至": "至",
|
||||
"过期于": "過期於",
|
||||
"作废于": "作廢於",
|
||||
"购买套餐后即可享受模型权益": "購買訂閱後即可享受模型權益",
|
||||
"限购": "限購",
|
||||
"推荐": "推薦",
|
||||
"已达到购买上限": "已達到購買上限",
|
||||
"已达上限": "已達上限",
|
||||
"立即订阅": "立即訂閱",
|
||||
"暂无可购买套餐": "暫無可購買訂閱",
|
||||
"该套餐未配置 Stripe": "該訂閱未設定 Stripe",
|
||||
"已打开支付页面": "已打開支付頁面",
|
||||
"支付失败": "支付失敗",
|
||||
"该套餐未配置 Creem": "該訂閱未設定 Creem",
|
||||
"已发起支付": "已發起支付",
|
||||
"购买订阅套餐": "購買訂閱",
|
||||
"套餐名称": "訂閱名稱",
|
||||
"应付金额": "應付金額",
|
||||
"支付": "支付",
|
||||
"管理员未开启在线支付功能,请联系管理员配置。": "管理員未開啟在線支付功能,請聯繫管理員設定。",
|
||||
"偏好设置": "偏好設定",
|
||||
"界面语言和其他个人偏好": "界面語言和其他個人偏好",
|
||||
"语言偏好": "語言偏好",
|
||||
"选择您的首选界面语言,设置将自动保存并同步到所有设备": "選擇您的首選界面語言,設定將自動儲存並同步到所有設備",
|
||||
"语言偏好已保存": "語言偏好已儲存",
|
||||
"提示:语言偏好会同步到您登录的所有设备,并影响API返回的错误消息语言。": "提示:語言偏好會同步到您登錄的所有設備,並影響API返回的錯誤消息語言。",
|
||||
"自定义 OAuth 提供商": "自訂 OAuth 提供商",
|
||||
"配置自定义 OAuth 提供商,支持 GitHub Enterprise、GitLab、Gitea、Nextcloud、Keycloak、ORY 等兼容 OAuth 2.0 协议的身份提供商": "設定自訂 OAuth 提供商,支援 GitHub Enterprise、GitLab、Gitea、Nextcloud、Keycloak、ORY 等兼容 OAuth 2.0 協議的身份提供商",
|
||||
"回调 URL 格式": "回調 URL 格式",
|
||||
"添加提供商": "添加提供商",
|
||||
"编辑提供商": "編輯提供商",
|
||||
"选择预设...": "選擇設定檔...",
|
||||
"输入基础 URL": "輸入基礎 URL",
|
||||
"例如": "例如",
|
||||
"提供商名称": "提供商名稱",
|
||||
"标识符 (Slug)": "標識符 (Slug)",
|
||||
"授权端点": "授權端點",
|
||||
"令牌端点": "令牌端點",
|
||||
"用户信息端点": "使用者資訊端點",
|
||||
"用户 ID 字段": "使用者 ID 字段",
|
||||
"支持 JSONPath,如 sub, id, data.user.id": "支援 JSONPath,如 sub, id, data.user.id",
|
||||
"用户名字段": "使用者名字段",
|
||||
"支持 JSONPath,如 preferred_username, login, data.user.username": "支援 JSONPath,如 preferred_username, login, data.user.username",
|
||||
"显示名称字段": "顯示名稱字段",
|
||||
"支持 JSONPath,如 name, display_name, data.user.name": "支援 JSONPath,如 name, display_name, data.user.name",
|
||||
"邮箱字段": "信箱字段",
|
||||
"支持 JSONPath,如 email, data.user.email": "支援 JSONPath,如 email, data.user.email",
|
||||
"授权范围 (Scopes)": "授權範圍 (Scopes)",
|
||||
"认证方式": "認證方式",
|
||||
"参数传递": "參數傳遞",
|
||||
"Basic Auth 头": "Basic Auth 頭",
|
||||
"暂无自定义 OAuth 提供商": "暫無自訂 OAuth 提供商",
|
||||
"确定要删除该提供商吗?": "確定要刪除該提供商嗎?",
|
||||
"确定要解绑 {{name}} 吗?": "確定要解綁 {{name}} 嗎?",
|
||||
"解绑成功": "解綁成功",
|
||||
"{{name}} ID": "{{name}} ID",
|
||||
"使用 {{name}} 继续": "使用 {{name}} 繼續",
|
||||
"端点 URL 必须以 http:// 或 https:// 开头:": "端點 URL 必須以 http:// 或 https:// 開頭:",
|
||||
"OAuth 配置错误:授权端点必须是完整的 URL(以 http:// 或 https:// 开头)": "OAuth 設定錯誤:授權端點必須是完整的 URL(以 http:// 或 https:// 開頭)",
|
||||
"OAuth 登录失败:": "OAuth 登錄失敗:",
|
||||
"必填:请输入服务器地址以自动生成完整端点 URL": "必填:請輸入伺服器位址以自動生成完整端點 URL",
|
||||
"填写服务器地址后自动生成:": "填寫伺服器位址後自動生成:",
|
||||
"自动生成:": "自動生成:",
|
||||
"请先填写服务器地址,以自动生成完整的端点 URL": "請先填寫伺服器位址,以自動生成完整的端點 URL",
|
||||
"端点 URL 必须是完整地址(以 http:// 或 https:// 开头)": "端點 URL 必須是完整位址(以 http:// 或 https:// 開頭)",
|
||||
"未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加": "未匹配到模型,按下 Enter 鍵可將「{{name}}」作為自訂模型名稱新增",
|
||||
"分组相关设置": "分組相關設定",
|
||||
"保存分组相关设置": "保存分組相關設定",
|
||||
"此页面仅显示未设置价格或基础倍率的模型,设置后会自动从列表中移出": "此頁面僅顯示未設定價格或基礎倍率的模型,設定後會自動從列表中移出",
|
||||
"没有未设置定价的模型": "沒有未設定定價的模型",
|
||||
"当前没有未设置定价的模型": "目前沒有未設定定價的模型",
|
||||
"模型计费编辑器": "模型計費編輯器",
|
||||
"价格摘要": "價格摘要",
|
||||
"当前提示": "目前提示",
|
||||
"这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。": "這個介面預設按價格填寫,儲存時會自動換算回後端需要的倍率 JSON。",
|
||||
"当前未启用,需要时再打开即可。": "目前未啟用,需要時再開啟即可。",
|
||||
"下面展示这个模型保存后会写入哪些后端字段,便于和原始 JSON 编辑框保持一致。": "下方會顯示此模型儲存後將寫入哪些後端欄位,方便與原始 JSON 編輯框保持一致。",
|
||||
"补全价格已锁定": "補全價格已鎖定",
|
||||
"后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。": "後端固定倍率:{{ratio}}。此欄位僅展示換算後的價格。",
|
||||
"这些价格都是可选项,不填也可以。": "這些價格都是可選項,不填也可以。",
|
||||
"请先开启并填写音频输入价格。": "請先開啟並填寫音訊輸入價格。",
|
||||
"输入模型名称,例如 gpt-4.1": "輸入模型名稱,例如 gpt-4.1",
|
||||
"当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。": "目前模型同時存在按次價格與倍率配置,儲存時會依目前計費方式覆蓋。",
|
||||
"当前模型存在未显式设置输入倍率的扩展倍率;填写输入价格后会自动换算为价格字段。": "目前模型存在未明確設定輸入倍率的擴展倍率;填寫輸入價格後會自動換算為價格欄位。",
|
||||
"按量计费下需要先填写输入价格,才能保存其它价格项。": "按量計費下需要先填寫輸入價格,才能儲存其它價格項。",
|
||||
"填写音频补全价格前,需要先填写音频输入价格。": "填寫音訊補全價格前,需要先填寫音訊輸入價格。",
|
||||
"模型 {{name}} 缺少输入价格,无法计算补全/缓存/图片/音频价格对应的倍率": "模型 {{name}} 缺少輸入價格,無法計算補全、快取、圖片與音訊價格對應的倍率",
|
||||
"模型 {{name}} 缺少音频输入价格,无法计算音频补全倍率": "模型 {{name}} 缺少音訊輸入價格,無法計算音訊補全倍率",
|
||||
"批量应用当前模型价格": "批量套用目前模型價格",
|
||||
"请先选择一个作为模板的模型": "請先選擇一個作為範本的模型",
|
||||
"请先勾选需要批量设置的模型": "請先勾選需要批量設定的模型",
|
||||
"已将模型 {{name}} 的价格配置批量应用到 {{count}} 个模型": "已將模型 {{name}} 的價格配置批量套用到 {{count}} 個模型",
|
||||
"将把当前编辑中的模型 {{name}} 的价格配置,批量应用到已勾选的 {{count}} 个模型。": "會把目前編輯中的模型 {{name}} 的價格配置,批量套用到已勾選的 {{count}} 個模型。",
|
||||
"适合同系列模型一起定价,例如把 gpt-5.1 的价格批量同步到 gpt-5.1-high、gpt-5.1-low 等模型。": "適合同系列模型一起定價,例如把 gpt-5.1 的價格批量同步到 gpt-5.1-high、gpt-5.1-low 等模型。",
|
||||
"已勾选": "已勾選",
|
||||
"当前编辑": "目前編輯",
|
||||
"已勾选 {{count}} 个模型": "已勾選 {{count}} 個模型",
|
||||
"基础价格": "基礎價格",
|
||||
"扩展价格": "擴展價格",
|
||||
"额外价格项": "額外價格項",
|
||||
"补全价格": "補全價格",
|
||||
"缓存读取价格": "快取讀取價格",
|
||||
"缓存创建价格": "快取建立價格",
|
||||
"图片输入价格": "圖片輸入價格",
|
||||
"音频输入价格": "音訊輸入價格",
|
||||
"音频补全价格": "音訊補全價格",
|
||||
"适合 MJ / 任务类等按次收费模型。": "適合 MJ / 任務類等按次收費模型。",
|
||||
"该模型补全倍率由后端固定为 {{ratio}}。补全价格不能在这里修改。": "該模型補全倍率由後端固定為 {{ratio}}。補全價格不能在這裡修改。",
|
||||
"计费显示模式": "計費顯示模式",
|
||||
"价格模式(默认)": "價格模式(預設)",
|
||||
"模型价格 {{symbol}}{{price}} / 次": "模型價格 {{symbol}}{{price}} / 次",
|
||||
"按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"模型价格:{{symbol}}{{price}} / 次": "模型價格:{{symbol}}{{price}} / 次",
|
||||
"按次:{{symbol}}{{price}}": "按次:{{symbol}}{{price}}",
|
||||
"实际结算金额:{{symbol}}{{total}}(已包含分组价格调整)": "實際結算金額:{{symbol}}{{total}}(已包含分組價格調整)",
|
||||
"缓存读取价格:{{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}}{{price}} / 1M tokens": "快取建立價格 {{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格:{{symbol}}{{price}} / 1M tokens": "5m快取建立價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"5m缓存创建价格 {{symbol}}{{price}} / 1M tokens": "5m快取建立價格 {{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格:{{symbol}}{{price}} / 1M tokens": "1h快取建立價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"1h缓存创建价格 {{symbol}}{{price}} / 1M tokens": "1h快取建立價格 {{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}}{{price}} / 1M tokens",
|
||||
"音频输入价格:{{symbol}}{{price}} / 1M tokens": "音訊輸入價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"音频补全价格:{{symbol}}{{price}} / 1M tokens": "音訊補全價格:{{symbol}}{{price}} / 1M tokens",
|
||||
"Web 搜索调用 {{webSearchCallCount}} 次": "Web 搜尋呼叫 {{webSearchCallCount}} 次",
|
||||
"文件搜索调用 {{fileSearchCallCount}} 次": "檔案搜尋呼叫 {{fileSearchCallCount}} 次",
|
||||
"图片倍率 {{imageRatio}}": "圖片倍率 {{imageRatio}}",
|
||||
"音频倍率 {{audioRatio}}": "音訊倍率 {{audioRatio}}",
|
||||
"普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "普通輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "快取輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 快取倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "圖片輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 圖片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音訊輸入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音訊倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 補全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "Web 搜尋:{{count}} / 1K * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "檔案搜尋:{{count}} / 1K * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}": "圖片生成:1 次 * 單價 {{price}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:{{total}}": "合計:{{total}}",
|
||||
"模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}": "模型倍率 {{modelRatio}},補全倍率 {{completionRatio}},音訊倍率 {{audioRatio}},音訊補全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}",
|
||||
"文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "文字輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 補全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "音訊輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音訊倍率 {{audioRatio}} * 音訊補全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}": "合計:文字部分 {{textTotal}} + 音訊部分 {{audioTotal}} = {{total}}",
|
||||
"模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}": "模型倍率 {{modelRatio}},輸出倍率 {{completionRatio}},快取倍率 {{cacheRatio}},{{ratioType}} {{ratio}}",
|
||||
"缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "快取讀取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 快取倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 快取建立倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}": "5m快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m快取建立倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}": "1h快取建立:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h快取建立倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}": "輸出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 輸出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}",
|
||||
"空": "空",
|
||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "提示:端點映射僅用於模型廣場展示,不會影響模型真實呼叫。如需配置真實呼叫,請前往「管道管理」。",
|
||||
"购买订阅获得模型额度/次数": "購買訂閱取得模型額度/次數",
|
||||
"生产环境 RSA 私钥 Base64 (PKCS#8 DER)": "正式環境 RSA 私鑰 Base64 (PKCS#8 DER)",
|
||||
"沙盒环境 RSA 私钥 Base64 (PKCS#8 DER)": "沙盒環境 RSA 私鑰 Base64 (PKCS#8 DER)",
|
||||
"生产环境 Waffo 公钥 Base64 (X.509 DER)": "正式環境 Waffo 公鑰 Base64 (X.509 DER)",
|
||||
"沙盒环境 Waffo 公钥 Base64 (X.509 DER)": "沙盒環境 Waffo 公鑰 Base64 (X.509 DER)",
|
||||
"支付方式类型": "付款方式類型",
|
||||
"支付方式名称": "付款方式名稱",
|
||||
"获取充值配置失败": "取得儲值設定失敗",
|
||||
"获取充值配置异常": "儲值設定異常",
|
||||
"{{ratioType}} {{ratio}}x": "{{ratioType}} {{ratio}}x",
|
||||
"模型价格:{{symbol}}{{price}}": "模型價格:{{symbol}}{{price}}",
|
||||
"模型价格 {{price}}": "模型價格 {{price}}",
|
||||
"缓存读 {{price}} / 1M tokens": "快取讀 {{price}} / 1M tokens",
|
||||
"5m缓存创建 {{price}} / 1M tokens": "5m快取建立 {{price}} / 1M tokens",
|
||||
"1h缓存创建 {{price}} / 1M tokens": "1h快取建立 {{price}} / 1M tokens",
|
||||
"缓存创建 {{price}} / 1M tokens": "快取建立 {{price}} / 1M tokens",
|
||||
"图片输入 {{price}} / 1M tokens": "圖片輸入 {{price}} / 1M tokens",
|
||||
"输入 {{price}} / 1M tokens": "輸入 {{price}} / 1M tokens",
|
||||
"缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "快取 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "快取建立 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "5m快取建立 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}": "1h快取建立 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"Key": "Key",
|
||||
"Key 摘要": "Key 摘要",
|
||||
"写": "寫",
|
||||
"异步任务退款": "非同步任務退款",
|
||||
"扣费": "扣費",
|
||||
"根据 Anthropic 协定,/v1/messages 的输入 tokens 仅统计非缓存输入,不包含缓存读取与缓存写入 tokens。": "根據 Anthropic 協定,/v1/messages 的輸入 tokens 僅統計非快取輸入,不包含快取讀取與快取寫入 tokens。",
|
||||
"渠道亲和性": "渠道親和性",
|
||||
"由订阅抵扣": "由訂閱抵扣",
|
||||
"缓存写": "快取寫",
|
||||
"缓存读": "快取讀",
|
||||
"规则": "規則",
|
||||
"订阅抵扣": "訂閱抵扣",
|
||||
"违规扣费": "違規扣費",
|
||||
"退款": "退款",
|
||||
"(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}": "(輸入 {{nonImageInput}} tokens + 圖片輸入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}",
|
||||
"图片输入价格:{{symbol}}{{total}} / 1M tokens": "圖片輸入價格:{{symbol}}{{total}} / 1M tokens",
|
||||
"文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字補全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音訊提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音訊補全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}": "模型價格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}",
|
||||
"缓存读取价格:{{symbol}}{{total}} / 1M tokens": "快取讀取價格:{{symbol}}{{total}} / 1M tokens",
|
||||
"补全 {{completion}} tokens * 输出倍率 {{completionRatio}}": "補全 {{completion}} tokens * 輸出倍率 {{completionRatio}}",
|
||||
"补全倍率 {{completionRatio}}": "補全倍率 {{completionRatio}}",
|
||||
"输入价格:{{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",
|
||||
"复制密钥": "複製金鑰",
|
||||
"复制连接信息": "複製連線資訊",
|
||||
"检测到剪贴板中的连接信息": "偵測到剪貼簿中的連線資訊",
|
||||
"自动填入": "自動填入",
|
||||
"忽略": "忽略",
|
||||
"从剪贴板粘贴配置": "從剪貼簿貼上設定",
|
||||
"剪贴板中未检测到连接信息": "剪貼簿中未偵測到連線資訊",
|
||||
"连接信息已填入": "連線資訊已填入",
|
||||
"无法读取剪贴板": "無法讀取剪貼簿",
|
||||
"页面渲染出错,请刷新页面重试": "頁面渲染出錯,請重新整理頁面重試",
|
||||
"刷新页面": "重新整理頁面",
|
||||
",时间:": ",時間:",
|
||||
",点击更新": ",點擊更新"
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Collapsible,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Tag,
|
||||
Typography,
|
||||
Popconfirm,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IconPlus, IconDelete } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
IconPlus,
|
||||
IconDelete,
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CardTable from '../../../../components/common/ui/CardTable';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -57,14 +64,106 @@ export function serializeGroupGroupRatio(rules) {
|
||||
: JSON.stringify(nested, null, 2);
|
||||
}
|
||||
|
||||
function GroupSection({ groupName, items, groupOptions, onUpdate, onRemove, onAdd, t }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='flex items-center justify-between cursor-pointer'
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: 'var(--semi-color-fill-0)',
|
||||
}}
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
{open ? <IconChevronUp size='small' /> : <IconChevronDown size='small' />}
|
||||
<Text strong>{groupName}</Text>
|
||||
<Tag size='small' color='blue'>{items.length} {t('条规则')}</Tag>
|
||||
</div>
|
||||
<div className='flex items-center gap-1' onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
size='small'
|
||||
theme='borderless'
|
||||
onClick={() => onAdd(groupName)}
|
||||
/>
|
||||
<Popconfirm
|
||||
title={t('确认删除该分组的所有规则?')}
|
||||
onConfirm={() => items.forEach((item) => onRemove(item._id))}
|
||||
position='left'
|
||||
>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
size='small'
|
||||
type='danger'
|
||||
theme='borderless'
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<Collapsible isOpen={open} keepDOM>
|
||||
<div style={{ padding: '8px 12px' }}>
|
||||
{items.map((rule) => (
|
||||
<div
|
||||
key={rule._id}
|
||||
className='flex items-center gap-2'
|
||||
style={{ marginBottom: 6 }}
|
||||
>
|
||||
<Select
|
||||
size='small'
|
||||
filter
|
||||
value={rule.usingGroup || undefined}
|
||||
placeholder={t('选择使用分组')}
|
||||
optionList={groupOptions}
|
||||
onChange={(v) => onUpdate(rule._id, 'usingGroup', v)}
|
||||
style={{ flex: 1 }}
|
||||
allowCreate
|
||||
position='bottomLeft'
|
||||
/>
|
||||
<InputNumber
|
||||
size='small'
|
||||
min={0}
|
||||
step={0.1}
|
||||
value={rule.ratio}
|
||||
style={{ width: 100 }}
|
||||
onChange={(v) => onUpdate(rule._id, 'ratio', v ?? 0)}
|
||||
/>
|
||||
<Popconfirm
|
||||
title={t('确认删除该规则?')}
|
||||
onConfirm={() => onRemove(rule._id)}
|
||||
position='left'
|
||||
>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type='danger'
|
||||
theme='borderless'
|
||||
size='small'
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GroupGroupRatioRules({
|
||||
value,
|
||||
groupNames = [],
|
||||
onChange,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [rules, setRules] = useState(() => flattenRules(parseJSON(value)));
|
||||
const [newGroupName, setNewGroupName] = useState('');
|
||||
|
||||
const emitChange = useCallback(
|
||||
(newRules) => {
|
||||
@@ -76,21 +175,11 @@ export default function GroupGroupRatioRules({
|
||||
|
||||
const updateRule = useCallback(
|
||||
(id, field, val) => {
|
||||
const next = rules.map((r) =>
|
||||
r._id === id ? { ...r, [field]: val } : r,
|
||||
);
|
||||
emitChange(next);
|
||||
emitChange(rules.map((r) => (r._id === id ? { ...r, [field]: val } : r)));
|
||||
},
|
||||
[rules, emitChange],
|
||||
);
|
||||
|
||||
const addRule = useCallback(() => {
|
||||
emitChange([
|
||||
...rules,
|
||||
{ _id: uid(), userGroup: '', usingGroup: '', ratio: 1 },
|
||||
]);
|
||||
}, [rules, emitChange]);
|
||||
|
||||
const removeRule = useCallback(
|
||||
(id) => {
|
||||
emitChange(rules.filter((r) => r._id !== id));
|
||||
@@ -98,107 +187,99 @@ export default function GroupGroupRatioRules({
|
||||
[rules, emitChange],
|
||||
);
|
||||
|
||||
const addRuleToGroup = useCallback(
|
||||
(groupName) => {
|
||||
emitChange([
|
||||
...rules,
|
||||
{ _id: uid(), userGroup: groupName, usingGroup: '', ratio: 1 },
|
||||
]);
|
||||
},
|
||||
[rules, emitChange],
|
||||
);
|
||||
|
||||
const addNewGroup = useCallback(() => {
|
||||
const name = newGroupName.trim();
|
||||
if (!name) return;
|
||||
emitChange([
|
||||
...rules,
|
||||
{ _id: uid(), userGroup: name, usingGroup: '', ratio: 1 },
|
||||
]);
|
||||
setNewGroupName('');
|
||||
}, [rules, emitChange, newGroupName]);
|
||||
|
||||
const groupOptions = useMemo(
|
||||
() => groupNames.map((n) => ({ value: n, label: n })),
|
||||
[groupNames],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t('用户分组'),
|
||||
dataIndex: 'userGroup',
|
||||
key: 'userGroup',
|
||||
width: 200,
|
||||
render: (_, record) => (
|
||||
const grouped = useMemo(() => {
|
||||
const map = {};
|
||||
const order = [];
|
||||
rules.forEach((r) => {
|
||||
if (!r.userGroup) return;
|
||||
if (!map[r.userGroup]) {
|
||||
map[r.userGroup] = [];
|
||||
order.push(r.userGroup);
|
||||
}
|
||||
map[r.userGroup].push(r);
|
||||
});
|
||||
return order.map((name) => ({ name, items: map[name] }));
|
||||
}, [rules]);
|
||||
|
||||
if (grouped.length === 0 && rules.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<Text type='tertiary' className='block text-center py-4'>
|
||||
{t('暂无规则,点击下方按钮添加')}
|
||||
</Text>
|
||||
<div className='mt-2 flex justify-center gap-2'>
|
||||
<Select
|
||||
size='small'
|
||||
filter
|
||||
value={record.userGroup || undefined}
|
||||
allowCreate
|
||||
placeholder={t('选择用户分组')}
|
||||
optionList={groupOptions}
|
||||
onChange={(v) => updateRule(record._id, 'userGroup', v)}
|
||||
style={{ width: '100%' }}
|
||||
allowCreate
|
||||
value={newGroupName || undefined}
|
||||
onChange={setNewGroupName}
|
||||
style={{ width: 200 }}
|
||||
position='bottomLeft'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('使用分组'),
|
||||
dataIndex: 'usingGroup',
|
||||
key: 'usingGroup',
|
||||
width: 200,
|
||||
render: (_, record) => (
|
||||
<Select
|
||||
size='small'
|
||||
filter
|
||||
value={record.usingGroup || undefined}
|
||||
placeholder={t('选择使用分组')}
|
||||
optionList={groupOptions}
|
||||
onChange={(v) => updateRule(record._id, 'usingGroup', v)}
|
||||
style={{ width: '100%' }}
|
||||
allowCreate
|
||||
position='bottomLeft'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('倍率'),
|
||||
dataIndex: 'ratio',
|
||||
key: 'ratio',
|
||||
width: 140,
|
||||
render: (_, record) => (
|
||||
<InputNumber
|
||||
size='small'
|
||||
min={0}
|
||||
step={0.1}
|
||||
value={record.ratio}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(v) => updateRule(record._id, 'ratio', v ?? 0)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
width: 50,
|
||||
render: (_, record) => (
|
||||
<Popconfirm
|
||||
title={t('确认删除该规则?')}
|
||||
onConfirm={() => removeRule(record._id)}
|
||||
position='left'
|
||||
>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type='danger'
|
||||
theme='borderless'
|
||||
size='small'
|
||||
/>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
],
|
||||
[t, groupOptions, updateRule, removeRule],
|
||||
);
|
||||
<Button icon={<IconPlus />} theme='outline' onClick={addNewGroup}>
|
||||
{t('添加分组规则')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CardTable
|
||||
columns={columns}
|
||||
dataSource={rules}
|
||||
rowKey='_id'
|
||||
hidePagination
|
||||
size='small'
|
||||
empty={
|
||||
<Text type='tertiary'>
|
||||
{t('暂无规则,点击下方按钮添加')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<div className='mt-3 flex justify-center'>
|
||||
<Button icon={<IconPlus />} theme='outline' onClick={addRule}>
|
||||
{t('添加规则')}
|
||||
<div className='space-y-2'>
|
||||
{grouped.map((group) => (
|
||||
<GroupSection
|
||||
key={group.name}
|
||||
groupName={group.name}
|
||||
items={group.items}
|
||||
groupOptions={groupOptions}
|
||||
onUpdate={updateRule}
|
||||
onRemove={removeRule}
|
||||
onAdd={addRuleToGroup}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
<div className='mt-3 flex justify-center gap-2'>
|
||||
<Select
|
||||
size='small'
|
||||
filter
|
||||
allowCreate
|
||||
placeholder={t('选择用户分组')}
|
||||
optionList={groupOptions}
|
||||
value={newGroupName || undefined}
|
||||
onChange={setNewGroupName}
|
||||
style={{ width: 200 }}
|
||||
position='bottomLeft'
|
||||
/>
|
||||
<Button icon={<IconPlus />} theme='outline' onClick={addNewGroup}>
|
||||
{t('添加分组规则')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,38 @@
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Collapsible,
|
||||
Input,
|
||||
Select,
|
||||
Tag,
|
||||
Typography,
|
||||
Popconfirm,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IconPlus, IconDelete } from '@douyinfe/semi-icons';
|
||||
import {
|
||||
IconPlus,
|
||||
IconDelete,
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CardTable from '../../../../components/common/ui/CardTable';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -21,12 +44,8 @@ const OP_REMOVE = 'remove';
|
||||
const OP_APPEND = 'append';
|
||||
|
||||
function parsePrefix(rawKey) {
|
||||
if (rawKey.startsWith('+:')) {
|
||||
return { op: OP_ADD, groupName: rawKey.slice(2) };
|
||||
}
|
||||
if (rawKey.startsWith('-:')) {
|
||||
return { op: OP_REMOVE, groupName: rawKey.slice(2) };
|
||||
}
|
||||
if (rawKey.startsWith('+:')) return { op: OP_ADD, groupName: rawKey.slice(2) };
|
||||
if (rawKey.startsWith('-:')) return { op: OP_REMOVE, groupName: rawKey.slice(2) };
|
||||
return { op: OP_APPEND, groupName: rawKey };
|
||||
}
|
||||
|
||||
@@ -38,11 +57,7 @@ function toRawKey(op, groupName) {
|
||||
|
||||
function parseJSON(str) {
|
||||
if (!str || !str.trim()) return {};
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
try { return JSON.parse(str); } catch { return {}; }
|
||||
}
|
||||
|
||||
function flattenRules(nested) {
|
||||
@@ -68,17 +83,14 @@ function nestRules(rules) {
|
||||
rules.forEach(({ userGroup, op, targetGroup, description }) => {
|
||||
if (!userGroup || !targetGroup) return;
|
||||
if (!result[userGroup]) result[userGroup] = {};
|
||||
const key = toRawKey(op, targetGroup);
|
||||
result[userGroup][key] = description;
|
||||
result[userGroup][toRawKey(op, targetGroup)] = description;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function serializeGroupSpecialUsable(rules) {
|
||||
const nested = nestRules(rules);
|
||||
return Object.keys(nested).length === 0
|
||||
? ''
|
||||
: JSON.stringify(nested, null, 2);
|
||||
return Object.keys(nested).length === 0 ? '' : JSON.stringify(nested, null, 2);
|
||||
}
|
||||
|
||||
const OP_TAG_MAP = {
|
||||
@@ -87,14 +99,118 @@ const OP_TAG_MAP = {
|
||||
[OP_APPEND]: { color: 'blue', label: '追加' },
|
||||
};
|
||||
|
||||
function UsableGroupSection({ groupName, items, opOptions, onUpdate, onRemove, onAdd, t }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--semi-color-border)',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className='flex items-center justify-between cursor-pointer'
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
background: 'var(--semi-color-fill-0)',
|
||||
}}
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
<div className='flex items-center gap-2'>
|
||||
{open ? <IconChevronUp size='small' /> : <IconChevronDown size='small' />}
|
||||
<Text strong>{groupName}</Text>
|
||||
<Tag size='small' color='blue'>{items.length} {t('条规则')}</Tag>
|
||||
</div>
|
||||
<div className='flex items-center gap-1' onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
size='small'
|
||||
theme='borderless'
|
||||
onClick={() => onAdd(groupName)}
|
||||
/>
|
||||
<Popconfirm
|
||||
title={t('确认删除该分组的所有规则?')}
|
||||
onConfirm={() => items.forEach((item) => onRemove(item._id))}
|
||||
position='left'
|
||||
>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
size='small'
|
||||
type='danger'
|
||||
theme='borderless'
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<Collapsible isOpen={open} keepDOM>
|
||||
<div style={{ padding: '8px 12px' }}>
|
||||
{items.map((rule) => (
|
||||
<div
|
||||
key={rule._id}
|
||||
className='flex items-center gap-2'
|
||||
style={{ marginBottom: 6 }}
|
||||
>
|
||||
<Select
|
||||
size='small'
|
||||
value={rule.op}
|
||||
optionList={opOptions}
|
||||
onChange={(v) => onUpdate(rule._id, 'op', v)}
|
||||
style={{ width: 120 }}
|
||||
renderSelectedItem={(optionNode) => {
|
||||
const info = OP_TAG_MAP[optionNode.value] || {};
|
||||
return <Tag size='small' color={info.color}>{optionNode.label}</Tag>;
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
size='small'
|
||||
value={rule.targetGroup}
|
||||
placeholder={t('分组名称')}
|
||||
onChange={(v) => onUpdate(rule._id, 'targetGroup', v)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
{rule.op !== OP_REMOVE ? (
|
||||
<Input
|
||||
size='small'
|
||||
value={rule.description}
|
||||
placeholder={t('分组描述')}
|
||||
onChange={(v) => onUpdate(rule._id, 'description', v)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text type='tertiary' size='small'>-</Text>
|
||||
</div>
|
||||
)}
|
||||
<Popconfirm
|
||||
title={t('确认删除该规则?')}
|
||||
onConfirm={() => onRemove(rule._id)}
|
||||
position='left'
|
||||
>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type='danger'
|
||||
theme='borderless'
|
||||
size='small'
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GroupSpecialUsableRules({
|
||||
value,
|
||||
groupNames = [],
|
||||
onChange,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [rules, setRules] = useState(() => flattenRules(parseJSON(value)));
|
||||
const [newGroupName, setNewGroupName] = useState('');
|
||||
|
||||
const emitChange = useCallback(
|
||||
(newRules) => {
|
||||
@@ -106,41 +222,46 @@ export default function GroupSpecialUsableRules({
|
||||
|
||||
const updateRule = useCallback(
|
||||
(id, field, val) => {
|
||||
const next = rules.map((r) => {
|
||||
if (r._id !== id) return r;
|
||||
const updated = { ...r, [field]: val };
|
||||
if (field === 'op' && val === OP_REMOVE) {
|
||||
updated.description = 'remove';
|
||||
} else if (field === 'op' && r.op === OP_REMOVE && val !== OP_REMOVE) {
|
||||
if (updated.description === 'remove') updated.description = '';
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
emitChange(next);
|
||||
emitChange(
|
||||
rules.map((r) => {
|
||||
if (r._id !== id) return r;
|
||||
const updated = { ...r, [field]: val };
|
||||
if (field === 'op' && val === OP_REMOVE) updated.description = 'remove';
|
||||
else if (field === 'op' && r.op === OP_REMOVE && val !== OP_REMOVE) {
|
||||
if (updated.description === 'remove') updated.description = '';
|
||||
}
|
||||
return updated;
|
||||
}),
|
||||
);
|
||||
},
|
||||
[rules, emitChange],
|
||||
);
|
||||
|
||||
const addRule = useCallback(() => {
|
||||
emitChange([
|
||||
...rules,
|
||||
{
|
||||
_id: uid(),
|
||||
userGroup: '',
|
||||
op: OP_APPEND,
|
||||
targetGroup: '',
|
||||
description: '',
|
||||
},
|
||||
]);
|
||||
}, [rules, emitChange]);
|
||||
|
||||
const removeRule = useCallback(
|
||||
(id) => {
|
||||
emitChange(rules.filter((r) => r._id !== id));
|
||||
(id) => emitChange(rules.filter((r) => r._id !== id)),
|
||||
[rules, emitChange],
|
||||
);
|
||||
|
||||
const addRuleToGroup = useCallback(
|
||||
(groupName) => {
|
||||
emitChange([
|
||||
...rules,
|
||||
{ _id: uid(), userGroup: groupName, op: OP_APPEND, targetGroup: '', description: '' },
|
||||
]);
|
||||
},
|
||||
[rules, emitChange],
|
||||
);
|
||||
|
||||
const addNewGroup = useCallback(() => {
|
||||
const name = newGroupName.trim();
|
||||
if (!name) return;
|
||||
emitChange([
|
||||
...rules,
|
||||
{ _id: uid(), userGroup: name, op: OP_APPEND, targetGroup: '', description: '' },
|
||||
]);
|
||||
setNewGroupName('');
|
||||
}, [rules, emitChange, newGroupName]);
|
||||
|
||||
const groupOptions = useMemo(
|
||||
() => groupNames.map((n) => ({ value: n, label: n })),
|
||||
[groupNames],
|
||||
@@ -155,120 +276,74 @@ export default function GroupSpecialUsableRules({
|
||||
[t],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t('用户分组'),
|
||||
dataIndex: 'userGroup',
|
||||
key: 'userGroup',
|
||||
width: 180,
|
||||
render: (_, record) => (
|
||||
const grouped = useMemo(() => {
|
||||
const map = {};
|
||||
const order = [];
|
||||
rules.forEach((r) => {
|
||||
if (!r.userGroup) return;
|
||||
if (!map[r.userGroup]) {
|
||||
map[r.userGroup] = [];
|
||||
order.push(r.userGroup);
|
||||
}
|
||||
map[r.userGroup].push(r);
|
||||
});
|
||||
return order.map((name) => ({ name, items: map[name] }));
|
||||
}, [rules]);
|
||||
|
||||
if (grouped.length === 0 && rules.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<Text type='tertiary' className='block text-center py-4'>
|
||||
{t('暂无规则,点击下方按钮添加')}
|
||||
</Text>
|
||||
<div className='mt-2 flex justify-center gap-2'>
|
||||
<Select
|
||||
size='small'
|
||||
filter
|
||||
value={record.userGroup || undefined}
|
||||
allowCreate
|
||||
placeholder={t('选择用户分组')}
|
||||
optionList={groupOptions}
|
||||
onChange={(v) => updateRule(record._id, 'userGroup', v)}
|
||||
style={{ width: '100%' }}
|
||||
allowCreate
|
||||
value={newGroupName || undefined}
|
||||
onChange={setNewGroupName}
|
||||
style={{ width: 200 }}
|
||||
position='bottomLeft'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
dataIndex: 'op',
|
||||
key: 'op',
|
||||
width: 140,
|
||||
render: (_, record) => (
|
||||
<Select
|
||||
size='small'
|
||||
value={record.op}
|
||||
optionList={opOptions}
|
||||
onChange={(v) => updateRule(record._id, 'op', v)}
|
||||
style={{ width: '100%' }}
|
||||
renderSelectedItem={(optionNode) => {
|
||||
const tagInfo = OP_TAG_MAP[optionNode.value] || {};
|
||||
return (
|
||||
<Tag size='small' color={tagInfo.color}>
|
||||
{optionNode.label}
|
||||
</Tag>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('目标分组'),
|
||||
dataIndex: 'targetGroup',
|
||||
key: 'targetGroup',
|
||||
width: 180,
|
||||
render: (_, record) => (
|
||||
<Input
|
||||
size='small'
|
||||
value={record.targetGroup}
|
||||
placeholder={t('分组名称')}
|
||||
onChange={(v) => updateRule(record._id, 'targetGroup', v)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('描述'),
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
render: (_, record) =>
|
||||
record.op === OP_REMOVE ? (
|
||||
<Text type='tertiary' size='small'>-</Text>
|
||||
) : (
|
||||
<Input
|
||||
size='small'
|
||||
value={record.description}
|
||||
placeholder={t('分组描述')}
|
||||
onChange={(v) => updateRule(record._id, 'description', v)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
width: 50,
|
||||
render: (_, record) => (
|
||||
<Popconfirm
|
||||
title={t('确认删除该规则?')}
|
||||
onConfirm={() => removeRule(record._id)}
|
||||
position='left'
|
||||
>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type='danger'
|
||||
theme='borderless'
|
||||
size='small'
|
||||
/>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
],
|
||||
[t, groupOptions, opOptions, updateRule, removeRule],
|
||||
);
|
||||
<Button icon={<IconPlus />} theme='outline' onClick={addNewGroup}>
|
||||
{t('添加分组规则')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CardTable
|
||||
columns={columns}
|
||||
dataSource={rules}
|
||||
rowKey='_id'
|
||||
hidePagination
|
||||
size='small'
|
||||
empty={
|
||||
<Text type='tertiary'>
|
||||
{t('暂无规则,点击下方按钮添加')}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
<div className='mt-3 flex justify-center'>
|
||||
<Button icon={<IconPlus />} theme='outline' onClick={addRule}>
|
||||
{t('添加规则')}
|
||||
<div className='space-y-2'>
|
||||
{grouped.map((group) => (
|
||||
<UsableGroupSection
|
||||
key={group.name}
|
||||
groupName={group.name}
|
||||
items={group.items}
|
||||
opOptions={opOptions}
|
||||
onUpdate={updateRule}
|
||||
onRemove={removeRule}
|
||||
onAdd={addRuleToGroup}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
<div className='mt-3 flex justify-center gap-2'>
|
||||
<Select
|
||||
size='small'
|
||||
filter
|
||||
allowCreate
|
||||
placeholder={t('选择用户分组')}
|
||||
optionList={groupOptions}
|
||||
value={newGroupName || undefined}
|
||||
onChange={setNewGroupName}
|
||||
style={{ width: 200 }}
|
||||
position='bottomLeft'
|
||||
/>
|
||||
<Button icon={<IconPlus />} theme='outline' onClick={addNewGroup}>
|
||||
{t('添加分组规则')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import React, { useState, useCallback, useMemo, useRef } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -61,60 +61,63 @@ export function serializeGroupTable(rows) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function GroupTable({
|
||||
groupRatio,
|
||||
userUsableGroups,
|
||||
onChange,
|
||||
}) {
|
||||
export default function GroupTable({ groupRatio, userUsableGroups, onChange }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [rows, setRows] = useState(() =>
|
||||
buildRows(groupRatio, userUsableGroups),
|
||||
);
|
||||
|
||||
const emitChange = useCallback(
|
||||
(newRows) => {
|
||||
setRows(newRows);
|
||||
onChange?.(serializeGroupTable(newRows));
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
// Use functional setRows to keep updateRow/addRow/removeRow referentially
|
||||
// stable, preventing columns useMemo from rebuilding on every keystroke
|
||||
// which causes the Input cursor to jump to end (cursor reset bug).
|
||||
const onChangeRef = useRef(onChange);
|
||||
onChangeRef.current = onChange;
|
||||
|
||||
const emitAndSet = useCallback((updater) => {
|
||||
setRows((prev) => {
|
||||
const next = typeof updater === 'function' ? updater(prev) : updater;
|
||||
onChangeRef.current?.(serializeGroupTable(next));
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateRow = useCallback(
|
||||
(id, field, value) => {
|
||||
const next = rows.map((r) =>
|
||||
r._id === id ? { ...r, [field]: value } : r,
|
||||
emitAndSet((prev) =>
|
||||
prev.map((r) => (r._id === id ? { ...r, [field]: value } : r)),
|
||||
);
|
||||
emitChange(next);
|
||||
},
|
||||
[rows, emitChange],
|
||||
[emitAndSet],
|
||||
);
|
||||
|
||||
const addRow = useCallback(() => {
|
||||
const existingNames = new Set(rows.map((r) => r.name));
|
||||
let counter = 1;
|
||||
let newName = `group_${counter}`;
|
||||
while (existingNames.has(newName)) {
|
||||
counter++;
|
||||
newName = `group_${counter}`;
|
||||
}
|
||||
emitChange([
|
||||
...rows,
|
||||
{
|
||||
_id: uid(),
|
||||
name: newName,
|
||||
ratio: 1,
|
||||
selectable: true,
|
||||
description: '',
|
||||
},
|
||||
]);
|
||||
}, [rows, emitChange]);
|
||||
emitAndSet((prev) => {
|
||||
const existingNames = new Set(prev.map((r) => r.name));
|
||||
let counter = 1;
|
||||
let newName = `group_${counter}`;
|
||||
while (existingNames.has(newName)) {
|
||||
counter++;
|
||||
newName = `group_${counter}`;
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
_id: uid(),
|
||||
name: newName,
|
||||
ratio: 1,
|
||||
selectable: true,
|
||||
description: '',
|
||||
},
|
||||
];
|
||||
});
|
||||
}, [emitAndSet]);
|
||||
|
||||
const removeRow = useCallback(
|
||||
(id) => {
|
||||
emitChange(rows.filter((r) => r._id !== id));
|
||||
emitAndSet((prev) => prev.filter((r) => r._id !== id));
|
||||
},
|
||||
[rows, emitChange],
|
||||
[emitAndSet],
|
||||
);
|
||||
|
||||
const groupNames = useMemo(() => rows.map((r) => r.name), [rows]);
|
||||
@@ -127,6 +130,11 @@ export default function GroupTable({
|
||||
return new Set(Object.keys(counts).filter((k) => counts[k] > 1));
|
||||
}, [groupNames]);
|
||||
|
||||
// Use ref so column render functions always read the latest duplicate set
|
||||
// without adding duplicateNames to columns deps (which would break cursor).
|
||||
const duplicateNamesRef = useRef(duplicateNames);
|
||||
duplicateNamesRef.current = duplicateNames;
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -138,7 +146,9 @@ export default function GroupTable({
|
||||
<Input
|
||||
size='small'
|
||||
value={record.name}
|
||||
status={duplicateNames.has(record.name) ? 'warning' : undefined}
|
||||
status={
|
||||
duplicateNamesRef.current.has(record.name) ? 'warning' : undefined
|
||||
}
|
||||
onChange={(v) => updateRow(record._id, 'name', v)}
|
||||
/>
|
||||
),
|
||||
@@ -212,7 +222,7 @@ export default function GroupTable({
|
||||
),
|
||||
},
|
||||
],
|
||||
[t, duplicateNames, updateRow, removeRow],
|
||||
[t, updateRow, removeRow],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -223,9 +233,7 @@ export default function GroupTable({
|
||||
rowKey='_id'
|
||||
hidePagination
|
||||
size='small'
|
||||
empty={
|
||||
<Text type='tertiary'>{t('暂无分组,点击下方按钮添加')}</Text>
|
||||
}
|
||||
empty={<Text type='tertiary'>{t('暂无分组,点击下方按钮添加')}</Text>}
|
||||
/>
|
||||
<div className='mt-3 flex justify-center'>
|
||||
<Button icon={<IconPlus />} theme='outline' onClick={addRow}>
|
||||
@@ -234,7 +242,8 @@ export default function GroupTable({
|
||||
</div>
|
||||
{duplicateNames.size > 0 && (
|
||||
<Text type='warning' size='small' className='mt-2 block'>
|
||||
{t('存在重复的分组名称:')}{Array.from(duplicateNames).join(', ')}
|
||||
{t('存在重复的分组名称:')}
|
||||
{Array.from(duplicateNames).join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user