diff --git a/controller/passkey.go b/controller/passkey.go index d37fb9f..bbd24b4 100644 --- a/controller/passkey.go +++ b/controller/passkey.go @@ -36,6 +36,10 @@ func PasskeyRegisterBegin(c *gin.Context) { return } + if !requirePasskeyRegistrationVerification(c, user.Id) { + return + } + credential, err := model.GetPasskeyByUserID(user.Id) if err != nil && !errors.Is(err, model.ErrPasskeyNotFound) { common.ApiError(c, err) @@ -96,6 +100,10 @@ func PasskeyRegisterFinish(c *gin.Context) { return } + if !requirePasskeyRegistrationVerification(c, user.Id) { + return + } + wa, err := passkeysvc.BuildWebAuthn(c.Request) if err != nil { common.ApiError(c, err) @@ -151,6 +159,10 @@ func PasskeyDelete(c *gin.Context) { return } + if !requirePasskeyDeleteVerification(c, user.Id) { + return + } + if err := model.DeletePasskeyByUserID(user.Id); err != nil { common.ApiError(c, err) return @@ -474,6 +486,7 @@ func PasskeyVerifyFinish(c *gin.Context) { // Mark passkey as ready; /api/verify will convert this into the final secure verification session. session.Set(PasskeyReadySessionKey, time.Now().Unix()) session.Delete(SecureVerificationSessionKey) + session.Delete(secureVerificationMethodSessionKey) if err := session.Save(); err != nil { common.ApiError(c, fmt.Errorf("保存验证状态失败: %v", err)) return @@ -485,6 +498,64 @@ func PasskeyVerifyFinish(c *gin.Context) { }) } + +func requirePasskeyRegistrationVerification(c *gin.Context, userID int) bool { + twoFA, err := model.GetTwoFAByUserId(userID) + if err != nil { + common.ApiError(c, err) + return false + } + if twoFA == nil || !twoFA.IsEnabled { + return true + } + return requireSecureVerificationMethod(c, secureVerificationMethod2FA) +} + +func requirePasskeyDeleteVerification(c *gin.Context, userID int) bool { + twoFA, err := model.GetTwoFAByUserId(userID) + if err != nil { + common.ApiError(c, err) + return false + } + if twoFA != nil && twoFA.IsEnabled { + return requireSecureVerificationMethod(c, secureVerificationMethod2FA) + } + + _, err = model.GetPasskeyByUserID(userID) + if err != nil { + if errors.Is(err, model.ErrPasskeyNotFound) { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "该用户尚未绑定 Passkey", + }) + return false + } + common.ApiError(c, err) + return false + } + + return requireSecureVerificationMethod(c, secureVerificationMethodPasskey) +} + +func requireSecureVerificationMethod(c *gin.Context, method string) bool { + session := sessions.Default(c) + verifiedAt, ok := session.Get(SecureVerificationSessionKey).(int64) + if !ok || time.Now().Unix()-verifiedAt >= SecureVerificationTimeout { + session.Delete(SecureVerificationSessionKey) + session.Delete(secureVerificationMethodSessionKey) + _ = session.Save() + common.ApiErrorMsg(c, "请先完成安全验证") + return false + } + + if verifiedMethod, ok := session.Get(secureVerificationMethodSessionKey).(string); !ok || verifiedMethod != method { + common.ApiErrorMsg(c, "请先完成对应的安全验证") + return false + } + + return true +} + func getSessionUser(c *gin.Context) (*model.User, error) { session := sessions.Default(c) idRaw := session.Get("id") diff --git a/controller/topup.go b/controller/topup.go index e7a392a..53958bc 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -358,7 +358,7 @@ func EpayNotify(c *gin.Context) { return } log.Printf("易支付回调更新用户成功 %v", topUp) - model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", logger.LogQuota(quotaToAdd), topUp.Money)) + model.RecordTopupLog(topUp.UserId, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", logger.LogQuota(quotaToAdd), topUp.Money), c.ClientIP(), topUp.PaymentMethod, "epay") } } else { log.Printf("易支付异常回调: %v", verifyInfo) @@ -457,7 +457,7 @@ func AdminCompleteTopUp(c *gin.Context) { LockOrder(req.TradeNo) defer UnlockOrder(req.TradeNo) - if err := model.ManualCompleteTopUp(req.TradeNo); err != nil { + if err := model.ManualCompleteTopUp(req.TradeNo, c.ClientIP()); err != nil { common.ApiError(c, err) return } diff --git a/controller/topup_creem.go b/controller/topup_creem.go index 54b67b8..915ef80 100644 --- a/controller/topup_creem.go +++ b/controller/topup_creem.go @@ -352,7 +352,7 @@ func handleCheckoutCompleted(c *gin.Context, event *CreemWebhookEvent) { log.Printf("警告:Creem回调中客户姓名为空 - 订单号: %s", referenceId) } - err := model.RechargeCreem(referenceId, customerEmail, customerName) + err := model.RechargeCreem(referenceId, customerEmail, customerName, c.ClientIP()) if err != nil { log.Printf("Creem充值处理失败: %s, 订单号: %s", err.Error(), referenceId) c.AbortWithStatus(http.StatusInternalServerError) diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go index e1718cc..bb8e24f 100644 --- a/controller/topup_stripe.go +++ b/controller/topup_stripe.go @@ -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, }) @@ -165,11 +170,16 @@ func StripeWebhook(c *gin.Context) { return } + callerIp := c.ClientIP() switch event.Type { case stripe.EventTypeCheckoutSessionCompleted: - sessionCompleted(event) + sessionCompleted(event, callerIp) case stripe.EventTypeCheckoutSessionExpired: sessionExpired(event) + case stripe.EventTypeCheckoutSessionAsyncPaymentSucceeded: + sessionAsyncPaymentSucceeded(event, callerIp) + case stripe.EventTypeCheckoutSessionAsyncPaymentFailed: + sessionAsyncPaymentFailed(event, callerIp) default: log.Printf("不支持的Stripe Webhook事件类型: %s\n", event.Type) } @@ -177,7 +187,7 @@ func StripeWebhook(c *gin.Context) { c.Status(http.StatusOK) } -func sessionCompleted(event stripe.Event) { +func sessionCompleted(event stripe.Event, callerIp string) { customerId := event.GetObjectValue("customer") referenceId := event.GetObjectValue("client_reference_id") status := event.GetObjectValue("status") @@ -186,7 +196,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, callerIp) +} + +// sessionAsyncPaymentSucceeded handles delayed payment methods (bank transfer, SEPA, etc.) +// that confirm payment after the checkout session completes. +func sessionAsyncPaymentSucceeded(event stripe.Event, callerIp string) { + customerId := event.GetObjectValue("customer") + referenceId := event.GetObjectValue("client_reference_id") + log.Printf("Stripe 异步支付成功: %s", referenceId) + + fulfillOrder(event, referenceId, customerId, callerIp) +} + +// sessionAsyncPaymentFailed marks orders as failed when delayed payment methods +// ultimately fail (e.g. bank transfer not received, SEPA rejected). +func sessionAsyncPaymentFailed(event stripe.Event, callerIp string) { + 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, callerIp string) { + if len(referenceId) == 0 { + log.Println("未提供支付单号") + return + } + LockOrder(referenceId) defer UnlockOrder(referenceId) payload := map[string]any{ @@ -202,7 +275,7 @@ func sessionCompleted(event stripe.Event) { return } - err := model.Recharge(referenceId, customerId) + err := model.Recharge(referenceId, customerId, callerIp) if err != nil { log.Println(err.Error(), referenceId) return diff --git a/controller/topup_waffo.go b/controller/topup_waffo.go index fce3764..f55baef 100644 --- a/controller/topup_waffo.go +++ b/controller/topup_waffo.go @@ -357,7 +357,7 @@ func handleWaffoPayment(c *gin.Context, wh *core.WebhookHandler, result *core.Pa LockOrder(merchantOrderId) defer UnlockOrder(merchantOrderId) - if err := model.RechargeWaffo(merchantOrderId); err != nil { + if err := model.RechargeWaffo(merchantOrderId, c.ClientIP()); err != nil { log.Printf("Waffo 充值处理失败: %v, 订单: %s", err, merchantOrderId) sendWaffoWebhookResponse(c, wh, false, err.Error()) return diff --git a/model/log.go b/model/log.go index d42d717..1cb3b68 100644 --- a/model/log.go +++ b/model/log.go @@ -82,6 +82,33 @@ func RecordLog(userId int, logType int, content string) { } } +func RecordTopupLog(userId int, content string, callerIp string, paymentMethod string, callbackPaymentMethod string) { + username, _ := GetUsernameById(userId, false) + adminInfo := map[string]interface{}{ + "server_ip": common.GetIp(), + "caller_ip": callerIp, + "payment_method": paymentMethod, + "callback_payment_method": callbackPaymentMethod, + "version": common.Version, + } + other := map[string]interface{}{ + "admin_info": adminInfo, + } + log := &Log{ + UserId: userId, + Username: username, + CreatedAt: common.GetTimestamp(), + Type: LogTypeTopup, + Content: content, + Ip: callerIp, + Other: common.MapToJsonStr(other), + } + err := LOG_DB.Create(log).Error + if err != nil { + common.SysLog("failed to record topup log: " + err.Error()) + } +} + func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, tokenName string, content string, tokenId int, useTimeSeconds int, isStream bool, group string, other map[string]interface{}) { logger.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content)) diff --git a/model/topup.go b/model/topup.go index d8c92bf..1c4dc1b 100644 --- a/model/topup.go +++ b/model/topup.go @@ -23,6 +23,8 @@ type TopUp struct { Status string `json:"status"` } +var ErrPaymentMethodMismatch = errors.New("payment method mismatch") + func (topUp *TopUp) Insert() error { var err error err = DB.Create(topUp).Error @@ -55,7 +57,7 @@ func GetTopUpByTradeNo(tradeNo string) *TopUp { return topUp } -func Recharge(referenceId string, customerId string) (err error) { +func Recharge(referenceId string, customerId string, callerIp string) (err error) { if referenceId == "" { return errors.New("未提供支付单号") } @@ -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("充值订单状态错误") } @@ -99,7 +105,7 @@ func Recharge(referenceId string, customerId string) (err error) { return errors.New("充值失败,请稍后重试") } - RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", logger.FormatQuota(int(quota)), topUp.Amount)) + RecordTopupLog(topUp.UserId, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", logger.FormatQuota(int(quota)), topUp.Amount), callerIp, topUp.PaymentMethod, "stripe") return nil } @@ -236,7 +242,7 @@ func SearchAllTopUps(keyword string, pageInfo *common.PageInfo) (topups []*TopUp } // ManualCompleteTopUp 管理员手动完成订单并给用户充值 -func ManualCompleteTopUp(tradeNo string) error { +func ManualCompleteTopUp(tradeNo string, callerIp string) error { if tradeNo == "" { return errors.New("未提供订单号") } @@ -249,15 +255,14 @@ func ManualCompleteTopUp(tradeNo string) error { var userId int var quotaToAdd int var payMoney float64 + var paymentMethod string err := DB.Transaction(func(tx *gorm.DB) error { topUp := &TopUp{} - // 行级锁,避免并发补单 if err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", tradeNo).First(topUp).Error; err != nil { return errors.New("充值订单不存在") } - // 幂等处理:已成功直接返回 if topUp.Status == common.TopUpStatusSuccess { return nil } @@ -266,9 +271,6 @@ func ManualCompleteTopUp(tradeNo string) error { return errors.New("订单状态不是待支付,无法补单") } - // 计算应充值额度: - // - Stripe 订单:Money 代表经分组倍率换算后的美元数量,直接 * QuotaPerUnit - // - 其他订单(如易支付):Amount 为美元数量,* QuotaPerUnit if topUp.PaymentMethod == "stripe" { dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) quotaToAdd = int(decimal.NewFromFloat(topUp.Money).Mul(dQuotaPerUnit).IntPart()) @@ -281,20 +283,19 @@ func ManualCompleteTopUp(tradeNo string) error { return errors.New("无效的充值额度") } - // 标记完成 topUp.CompleteTime = common.GetTimestamp() topUp.Status = common.TopUpStatusSuccess if err := tx.Save(topUp).Error; err != nil { return err } - // 增加用户额度(立即写库,保持一致性) if err := tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; err != nil { return err } userId = topUp.UserId payMoney = topUp.Money + paymentMethod = topUp.PaymentMethod return nil }) @@ -302,11 +303,11 @@ func ManualCompleteTopUp(tradeNo string) error { return err } - // 事务外记录日志,避免阻塞 - RecordLog(userId, LogTypeTopup, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney)) + RecordTopupLog(userId, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney), callerIp, paymentMethod, "admin") return nil } -func RechargeCreem(referenceId string, customerEmail string, customerName string) (err error) { + +func RechargeCreem(referenceId string, customerEmail string, customerName string, callerIp string) (err error) { if referenceId == "" { return errors.New("未提供支付单号") } @@ -325,6 +326,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("充值订单状态错误") } @@ -336,24 +341,18 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string return err } - // Creem 直接使用 Amount 作为充值额度(整数) quota = topUp.Amount - // 构建更新字段,优先使用邮箱,如果邮箱为空则使用用户名 updateFields := map[string]interface{}{ "quota": gorm.Expr("quota + ?", quota), } - // 如果有客户邮箱,尝试更新用户邮箱(仅当用户邮箱为空时) if customerEmail != "" { - // 先检查用户当前邮箱是否为空 var user User err = tx.Where("id = ?", topUp.UserId).First(&user).Error if err != nil { return err } - - // 如果用户邮箱为空,则更新为支付时使用的邮箱 if user.Email == "" { updateFields["email"] = customerEmail } @@ -372,12 +371,12 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string return errors.New("充值失败,请稍后重试") } - RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", quota, topUp.Money)) + RecordTopupLog(topUp.UserId, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", quota, topUp.Money), callerIp, topUp.PaymentMethod, "creem") return nil } -func RechargeWaffo(tradeNo string) (err error) { +func RechargeWaffo(tradeNo string, callerIp string) (err error) { if tradeNo == "" { return errors.New("未提供支付单号") } @@ -396,8 +395,12 @@ func RechargeWaffo(tradeNo string) (err error) { return errors.New("充值订单不存在") } + if topUp.PaymentMethod != "waffo" { + return ErrPaymentMethodMismatch + } + if topUp.Status == common.TopUpStatusSuccess { - return nil // 幂等:已成功直接返回 + return nil } if topUp.Status != common.TopUpStatusPending { @@ -430,7 +433,7 @@ func RechargeWaffo(tradeNo string) (err error) { } if quotaToAdd > 0 { - RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("Waffo充值成功,充值额度: %v,支付金额: %.2f", logger.FormatQuota(quotaToAdd), topUp.Money)) + RecordTopupLog(topUp.UserId, fmt.Sprintf("Waffo充值成功,充值额度: %v,支付金额: %.2f", logger.FormatQuota(quotaToAdd), topUp.Money), callerIp, topUp.PaymentMethod, "waffo") } return nil diff --git a/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx b/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx index 61104d2..ac8b284 100644 --- a/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx +++ b/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx @@ -830,7 +830,12 @@ export const getLogsColumns = ({ ), dataIndex: 'ip', render: (text, record, index) => { - return (record.type === 2 || record.type === 5) && text ? ( + const showIp = + (record.type === 2 || + record.type === 5 || + (isAdminUser && record.type === 1)) && + text; + return showIp ? ( { ), }); } - if (isAdminUser && logs[i].type !== 6) { + if (isAdminUser && logs[i].type !== 6 && logs[i].type !== 1) { expandDataLocal.push({ key: t('请求转换'), value: requestConversionDisplayValue(other?.request_conversion), }); } - if (isAdminUser && logs[i].type !== 6) { + if (isAdminUser && logs[i].type !== 6 && logs[i].type !== 1) { let localCountMode = ''; if (other?.admin_info?.local_count_tokens) { localCountMode = t('本地计费'); @@ -683,6 +683,39 @@ export const useLogsData = () => { value: localCountMode, }); } + if (isAdminUser && logs[i].type === 1 && other?.admin_info) { + const adminInfo = other.admin_info; + if (adminInfo.payment_method) { + expandDataLocal.push({ + key: t('订单支付方式'), + value: adminInfo.payment_method, + }); + } + if (adminInfo.callback_payment_method) { + expandDataLocal.push({ + key: t('回调支付方式'), + value: adminInfo.callback_payment_method, + }); + } + if (adminInfo.caller_ip) { + expandDataLocal.push({ + key: t('回调调用者IP'), + value: adminInfo.caller_ip, + }); + } + if (adminInfo.server_ip) { + expandDataLocal.push({ + key: t('服务器IP'), + value: adminInfo.server_ip, + }); + } + if (adminInfo.version) { + expandDataLocal.push({ + key: t('系统版本'), + value: adminInfo.version, + }); + } + } expandDatesLocal[logs[i].key] = expandDataLocal; } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 0a33043..a1e37a1 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -570,6 +570,18 @@ "先填写配置,再自动填充 OAuth 端点,能显著减少手工输入": "Fill in configuration first, then auto-fill OAuth endpoints to significantly reduce manual input", "先搜索,再一键复制字段名或填入当前规则。字段名为系统内部路径,可直接用于路径 / 来源 / 目标。": "Search first, then copy field names or fill into the current rule with one click. Field names are internal system paths that can be used directly for path / source / target.", "免责声明:仅限个人使用,请勿分发或共享任何凭证。该渠道存在前置条件与使用门槛,请在充分了解流程与风险后使用,并遵守 OpenAI 的相关条款与政策。相关凭证与配置仅限接入 Codex CLI 使用,不适用于其他客户端、平台或渠道。": "Disclaimer: Personal use only. Do not distribute or share any credentials. This channel has prerequisites and requires prior setup; use only if you understand the flow and risks, and comply with OpenAI’s terms and policies. Credentials and configuration are for Codex CLI integration only, and are not intended for any other client, platform, or channel.", + "该类型用于兼容上游把 OpenAI 官方兼容入口放在 /openai 的第三方站点。这里填写上游给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL,例如:https://api.univibe.cc/openai;下游客户端仍然使用你自己的 New API 地址加 /v1。": "This type is for third-party upstreams that expose the OpenAI official-compatible entrypoint under /openai. Fill in the upstream Base URL used by the official OpenAI client or Codex CLI, for example: https://api.univibe.cc/openai. Downstream clients should still use your own New API address plus /v1.", + "上游 OpenAI / Codex Base URL": "Upstream OpenAI / Codex Base URL", + "请输入上游提供给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL,例如:https://api.univibe.cc/openai": "Enter the upstream Base URL used by the official OpenAI client or Codex CLI, for example: https://api.univibe.cc/openai", + "此类型会把下游的 /v1/* 请求自动映射到上游的 /openai/*。这里不要填写 /v1,保持上游原始 Base URL 即可。": "This type automatically maps downstream /v1/* requests to upstream /openai/*. Do not append /v1 here; keep the original upstream Base URL.", + "该类型用于兼容 Claude Code 或 Anthropic 网关类上游。这里填写上游提供的 Anthropic Base URL,例如:https://gateway.example.com/anthropic;转发时会保持 Anthropic 的 /v1/messages 路径,但按网关常见要求使用 Bearer 认证。": "This type is for Claude Code or Anthropic gateway upstreams. Fill in the upstream Anthropic Base URL, for example: https://gateway.example.com/anthropic. Requests keep Anthropic's /v1/messages path while using Bearer authentication for gateway compatibility.", + "上游 Claude Code / Anthropic Base URL": "Upstream Claude Code / Anthropic Base URL", + "请输入上游提供给 Claude Code 使用的 Anthropic Base URL,例如:https://gateway.example.com/anthropic": "Enter the upstream Anthropic Base URL used by Claude Code, for example: https://gateway.example.com/anthropic", + "此类型保持 Anthropic 官方请求路径,不自动补 /v1 到 Base URL;这里直接填写上游原始 Base URL 即可。": "This type keeps Anthropic's official request path and does not append /v1 to the Base URL automatically. Enter the raw upstream Base URL directly.", + "该类型用于兼容 Gemini OpenAI 兼容入口。这里填写上游给 Gemini CLI 或 OpenAI 兼容 SDK 使用的 Base URL,例如:https://generativelanguage.googleapis.com/v1beta/openai;下游客户端仍然使用你自己的 New API 地址加 /v1。": "This type is for Gemini OpenAI-compatible upstreams. Fill in the Base URL used by Gemini CLI or OpenAI-compatible SDKs, for example: https://generativelanguage.googleapis.com/v1beta/openai. Downstream clients should still use your own New API address plus /v1.", + "上游 Gemini OpenAI Compat Base URL": "Upstream Gemini OpenAI Compat Base URL", + "请输入 Gemini OpenAI 兼容 Base URL,例如:https://generativelanguage.googleapis.com/v1beta/openai": "Enter the Gemini OpenAI-compatible Base URL, for example: https://generativelanguage.googleapis.com/v1beta/openai", + "此类型会把下游的 /v1/* 请求自动映射到上游的 /v1beta/openai/*。这里不要再额外填写 /v1。": "This type automatically maps downstream /v1/* requests to upstream /v1beta/openai/*. Do not append /v1 again here.", "兑换人ID": "Redeemer ID", "兑换成功!": "Redemption successful!", "兑换码充值": "Redemption code recharge", @@ -3339,6 +3351,11 @@ "输出价格:{{symbol}}{{price}} / 1M tokens": "Output Price: {{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{total}} / 1M tokens": "Output Price: {{symbol}}{{total}} / 1M tokens", "例如:gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$": "Example: gpt-4.1-nano,regex:^claude-.*$,regex:^sora-.*$", - "支持精确匹配;使用 regex: 开头可按正则匹配。": "Supports exact matching. Use a regex: prefix for regex matching." + "支持精确匹配;使用 regex: 开头可按正则匹配。": "Supports exact matching. Use a regex: prefix for regex matching.", + "订单支付方式": "Order payment method", + "回调支付方式": "Callback payment method", + "回调调用者IP": "Callback caller IP", + "服务器IP": "Server IP", + "系统版本": "System version" } } diff --git a/web/src/i18n/locales/zh-CN.json b/web/src/i18n/locales/zh-CN.json index 6b754cc..d686d1c 100644 --- a/web/src/i18n/locales/zh-CN.json +++ b/web/src/i18n/locales/zh-CN.json @@ -153,6 +153,18 @@ "SSRF防护详细说明": "SSRF防护可防止恶意用户利用您的服务器访问内网资源。您可以配置受信任域名/IP的白名单,并限制允许的端口。适用于文件下载、Webhook回调和通知功能。", "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用": "store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用", "免责声明:仅限个人使用,请勿分发或共享任何凭证。该渠道存在前置条件与使用门槛,请在充分了解流程与风险后使用,并遵守 OpenAI 的相关条款与政策。相关凭证与配置仅限接入 Codex CLI 使用,不适用于其他客户端、平台或渠道。": "免责声明:仅限个人使用,请勿分发或共享任何凭证。该渠道存在前置条件与使用门槛,请在充分了解流程与风险后使用,并遵守 OpenAI 的相关条款与政策。相关凭证与配置仅限接入 Codex CLI 使用,不适用于其他客户端、平台或渠道。", + "该类型用于兼容上游把 OpenAI 官方兼容入口放在 /openai 的第三方站点。这里填写上游给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL,例如:https://api.univibe.cc/openai;下游客户端仍然使用你自己的 New API 地址加 /v1。": "该类型用于兼容上游把 OpenAI 官方兼容入口放在 /openai 的第三方站点。这里填写上游给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL,例如:https://api.univibe.cc/openai;下游客户端仍然使用你自己的 New API 地址加 /v1。", + "上游 OpenAI / Codex Base URL": "上游 OpenAI / Codex Base URL", + "请输入上游提供给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL,例如:https://api.univibe.cc/openai": "请输入上游提供给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL,例如:https://api.univibe.cc/openai", + "此类型会把下游的 /v1/* 请求自动映射到上游的 /openai/*。这里不要填写 /v1,保持上游原始 Base URL 即可。": "此类型会把下游的 /v1/* 请求自动映射到上游的 /openai/*。这里不要填写 /v1,保持上游原始 Base URL 即可。", + "该类型用于兼容 Claude Code 或 Anthropic 网关类上游。这里填写上游提供的 Anthropic Base URL,例如:https://gateway.example.com/anthropic;转发时会保持 Anthropic 的 /v1/messages 路径,但按网关常见要求使用 Bearer 认证。": "该类型用于兼容 Claude Code 或 Anthropic 网关类上游。这里填写上游提供的 Anthropic Base URL,例如:https://gateway.example.com/anthropic;转发时会保持 Anthropic 的 /v1/messages 路径,但按网关常见要求使用 Bearer 认证。", + "上游 Claude Code / Anthropic Base URL": "上游 Claude Code / Anthropic Base URL", + "请输入上游提供给 Claude Code 使用的 Anthropic Base URL,例如:https://gateway.example.com/anthropic": "请输入上游提供给 Claude Code 使用的 Anthropic Base URL,例如:https://gateway.example.com/anthropic", + "此类型保持 Anthropic 官方请求路径,不自动补 /v1 到 Base URL;这里直接填写上游原始 Base URL 即可。": "此类型保持 Anthropic 官方请求路径,不自动补 /v1 到 Base URL;这里直接填写上游原始 Base URL 即可。", + "该类型用于兼容 Gemini OpenAI 兼容入口。这里填写上游给 Gemini CLI 或 OpenAI 兼容 SDK 使用的 Base URL,例如:https://generativelanguage.googleapis.com/v1beta/openai;下游客户端仍然使用你自己的 New API 地址加 /v1。": "该类型用于兼容 Gemini OpenAI 兼容入口。这里填写上游给 Gemini CLI 或 OpenAI 兼容 SDK 使用的 Base URL,例如:https://generativelanguage.googleapis.com/v1beta/openai;下游客户端仍然使用你自己的 New API 地址加 /v1。", + "上游 Gemini OpenAI Compat Base URL": "上游 Gemini OpenAI Compat Base URL", + "请输入 Gemini OpenAI 兼容 Base URL,例如:https://generativelanguage.googleapis.com/v1beta/openai": "请输入 Gemini OpenAI 兼容 Base URL,例如:https://generativelanguage.googleapis.com/v1beta/openai", + "此类型会把下游的 /v1/* 请求自动映射到上游的 /v1beta/openai/*。这里不要再额外填写 /v1。": "此类型会把下游的 /v1/* 请求自动映射到上游的 /v1beta/openai/*。这里不要再额外填写 /v1。", "Stripe 设置": "Stripe 设置", "Telegram": "Telegram", "Telegram Bot Token": "Telegram Bot Token", @@ -2956,6 +2968,11 @@ "输入价格:{{symbol}}{{price}} / 1M tokens": "输入价格:{{symbol}}{{price}} / 1M tokens", "输出价格 {{symbol}}{{price}} / 1M tokens": "输出价格 {{symbol}}{{price}} / 1M tokens", "输出价格:{{symbol}}{{price}} / 1M tokens": "输出价格:{{symbol}}{{price}} / 1M tokens", - "输出价格:{{symbol}}{{total}} / 1M tokens": "输出价格:{{symbol}}{{total}} / 1M tokens" + "输出价格:{{symbol}}{{total}} / 1M tokens": "输出价格:{{symbol}}{{total}} / 1M tokens", + "订单支付方式": "订单支付方式", + "回调支付方式": "回调支付方式", + "回调调用者IP": "回调调用者IP", + "服务器IP": "服务器IP", + "系统版本": "系统版本" } }