diff --git a/controller/topup.go b/controller/topup.go index 7e2cadf1d..76a2521d9 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -183,12 +183,13 @@ func RequestEpay(c *gin.Context) { amount = dAmount.Div(dQuotaPerUnit).IntPart() } topUp := &model.TopUp{ - UserId: id, - Amount: amount, - Money: payMoney, - TradeNo: tradeNo, - CreateTime: time.Now().Unix(), - Status: "pending", + UserId: id, + Amount: amount, + Money: payMoney, + TradeNo: tradeNo, + PaymentMethod: req.PaymentMethod, + CreateTime: time.Now().Unix(), + Status: "pending", } err = topUp.Insert() if err != nil { @@ -313,3 +314,76 @@ func RequestAmount(c *gin.Context) { } c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)}) } + +func GetUserTopUps(c *gin.Context) { + userId := c.GetInt("id") + pageInfo := common.GetPageQuery(c) + keyword := c.Query("keyword") + + var ( + topups []*model.TopUp + total int64 + err error + ) + if keyword != "" { + topups, total, err = model.SearchUserTopUps(userId, keyword, pageInfo) + } else { + topups, total, err = model.GetUserTopUps(userId, pageInfo) + } + if err != nil { + common.ApiError(c, err) + return + } + + pageInfo.SetTotal(int(total)) + pageInfo.SetItems(topups) + common.ApiSuccess(c, pageInfo) +} + +// GetAllTopUps 管理员获取全平台充值记录 +func GetAllTopUps(c *gin.Context) { + pageInfo := common.GetPageQuery(c) + keyword := c.Query("keyword") + + var ( + topups []*model.TopUp + total int64 + err error + ) + if keyword != "" { + topups, total, err = model.SearchAllTopUps(keyword, pageInfo) + } else { + topups, total, err = model.GetAllTopUps(pageInfo) + } + if err != nil { + common.ApiError(c, err) + return + } + + pageInfo.SetTotal(int(total)) + pageInfo.SetItems(topups) + common.ApiSuccess(c, pageInfo) +} + +type AdminCompleteTopupRequest struct { + TradeNo string `json:"trade_no"` +} + +// AdminCompleteTopUp 管理员补单接口 +func AdminCompleteTopUp(c *gin.Context) { + var req AdminCompleteTopupRequest + if err := c.ShouldBindJSON(&req); err != nil || req.TradeNo == "" { + common.ApiErrorMsg(c, "参数错误") + return + } + + // 订单级互斥,防止并发补单 + LockOrder(req.TradeNo) + defer UnlockOrder(req.TradeNo) + + if err := model.ManualCompleteTopUp(req.TradeNo); err != nil { + common.ApiError(c, err) + return + } + common.ApiSuccess(c, nil) +} diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go index 628a3fea5..a4bdf064d 100644 --- a/controller/topup_stripe.go +++ b/controller/topup_stripe.go @@ -83,12 +83,13 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) { } topUp := &model.TopUp{ - UserId: id, - Amount: req.Amount, - Money: chargedMoney, - TradeNo: referenceId, - CreateTime: time.Now().Unix(), - Status: common.TopUpStatusPending, + UserId: id, + Amount: req.Amount, + Money: chargedMoney, + TradeNo: referenceId, + PaymentMethod: PaymentMethodStripe, + CreateTime: time.Now().Unix(), + Status: common.TopUpStatusPending, } err = topUp.Insert() if err != nil { diff --git a/model/topup.go b/model/topup.go index 802c866f7..380f5851d 100644 --- a/model/topup.go +++ b/model/topup.go @@ -6,18 +6,20 @@ import ( "one-api/common" "one-api/logger" + "github.com/shopspring/decimal" "gorm.io/gorm" ) 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"` - 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"` } func (topUp *TopUp) Insert() error { @@ -99,3 +101,206 @@ func Recharge(referenceId string, customerId string) (err error) { return nil } + +func GetUserTopUps(userId int, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + // Start transaction + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // Get total count within transaction + err = tx.Model(&TopUp{}).Where("user_id = ?", userId).Count(&total).Error + if err != nil { + tx.Rollback() + return nil, 0, err + } + + // Get paginated topups within same transaction + err = tx.Where("user_id = ?", userId).Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error + if err != nil { + tx.Rollback() + return nil, 0, err + } + + // Commit transaction + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + + return topups, total, nil +} + +// GetAllTopUps 获取全平台的充值记录(管理员使用) +func GetAllTopUps(pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + if err = tx.Model(&TopUp{}).Count(&total).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + + return topups, total, nil +} + +// SearchUserTopUps 按订单号搜索某用户的充值记录 +func SearchUserTopUps(userId int, keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + query := tx.Model(&TopUp{}).Where("user_id = ?", userId) + if keyword != "" { + like := "%%" + keyword + "%%" + query = query.Where("trade_no LIKE ?", like) + } + + if err = query.Count(&total).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + return topups, total, nil +} + +// SearchAllTopUps 按订单号搜索全平台充值记录(管理员使用) +func SearchAllTopUps(keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + query := tx.Model(&TopUp{}) + if keyword != "" { + like := "%%" + keyword + "%%" + query = query.Where("trade_no LIKE ?", like) + } + + if err = query.Count(&total).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + return topups, total, nil +} + +// ManualCompleteTopUp 管理员手动完成订单并给用户充值 +func ManualCompleteTopUp(tradeNo string) error { + if tradeNo == "" { + return errors.New("未提供订单号") + } + + refCol := "`trade_no`" + if common.UsingPostgreSQL { + refCol = `"trade_no"` + } + + var userId int + var quotaToAdd int + var payMoney float64 + + 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 + } + + if topUp.Status != common.TopUpStatusPending { + 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()) + } else { + dAmount := decimal.NewFromInt(topUp.Amount) + dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) + quotaToAdd = int(dAmount.Mul(dQuotaPerUnit).IntPart()) + } + if quotaToAdd <= 0 { + 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 + return nil + }) + + if err != nil { + return err + } + + // 事务外记录日志,避免阻塞 + RecordLog(userId, LogTypeTopup, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney)) + return nil +} diff --git a/router/api-router.go b/router/api-router.go index d29615914..963abd105 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -73,6 +73,7 @@ func SetApiRouter(router *gin.Engine) { selfRoute.DELETE("/passkey", controller.PasskeyDelete) selfRoute.GET("/aff", controller.GetAffCode) selfRoute.GET("/topup/info", controller.GetTopUpInfo) + selfRoute.GET("/topup/self", controller.GetUserTopUps) selfRoute.POST("/topup", middleware.CriticalRateLimit(), controller.TopUp) selfRoute.POST("/pay", middleware.CriticalRateLimit(), controller.RequestEpay) selfRoute.POST("/amount", controller.RequestAmount) @@ -93,6 +94,8 @@ func SetApiRouter(router *gin.Engine) { adminRoute.Use(middleware.AdminAuth()) { adminRoute.GET("/", controller.GetAllUsers) + adminRoute.GET("/topup", controller.GetAllTopUps) + adminRoute.POST("/topup/complete", controller.AdminCompleteTopUp) adminRoute.GET("/search", controller.SearchUsers) adminRoute.GET("/:id", controller.GetUser) adminRoute.POST("/", controller.CreateUser) diff --git a/web/src/components/auth/LoginForm.jsx b/web/src/components/auth/LoginForm.jsx index 828e71786..fea4e39ba 100644 --- a/web/src/components/auth/LoginForm.jsx +++ b/web/src/components/auth/LoginForm.jsx @@ -42,7 +42,12 @@ import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import TelegramLoginButton from 'react-telegram-login'; -import { IconGithubLogo, IconMail, IconLock, IconKey } from '@douyinfe/semi-icons'; +import { + IconGithubLogo, + IconMail, + IconLock, + IconKey, +} from '@douyinfe/semi-icons'; import OIDCIcon from '../common/logo/OIDCIcon'; import WeChatIcon from '../common/logo/WeChatIcon'; import LinuxDoIcon from '../common/logo/LinuxDoIcon'; @@ -296,15 +301,22 @@ const LoginForm = () => { return; } - const publicKeyOptions = prepareCredentialRequestOptions(data?.options || data?.publicKey || data); - const assertion = await navigator.credentials.get({ publicKey: publicKeyOptions }); + const publicKeyOptions = prepareCredentialRequestOptions( + data?.options || data?.publicKey || data, + ); + const assertion = await navigator.credentials.get({ + publicKey: publicKeyOptions, + }); const payload = buildAssertionResult(assertion); if (!payload) { showError('Passkey 验证失败,请重试'); return; } - const finishRes = await API.post('/api/user/passkey/login/finish', payload); + const finishRes = await API.post( + '/api/user/passkey/login/finish', + payload, + ); const finish = finishRes.data; if (finish.success) { userDispatch({ type: 'login', payload: finish.data }); diff --git a/web/src/components/common/examples/ChannelKeyViewExample.jsx b/web/src/components/common/examples/ChannelKeyViewExample.jsx index cd3877527..1bb2998b2 100644 --- a/web/src/components/common/examples/ChannelKeyViewExample.jsx +++ b/web/src/components/common/examples/ChannelKeyViewExample.jsx @@ -58,7 +58,7 @@ const ChannelKeyViewExample = ({ channelId }) => { // 开始查看密钥流程 const handleViewKey = async () => { const apiCall = createApiCalls.viewChannelKey(channelId); - + await startVerification(apiCall, { title: t('查看渠道密钥'), description: t('为了保护账户安全,请验证您的身份。'), @@ -69,11 +69,7 @@ const ChannelKeyViewExample = ({ channelId }) => { return ( <> {/* 查看密钥按钮 */} - @@ -114,4 +110,4 @@ const ChannelKeyViewExample = ({ channelId }) => { ); }; -export default ChannelKeyViewExample; \ No newline at end of file +export default ChannelKeyViewExample; diff --git a/web/src/components/common/modals/SecureVerificationModal.jsx b/web/src/components/common/modals/SecureVerificationModal.jsx index 06f18c7e6..6c61c291d 100644 --- a/web/src/components/common/modals/SecureVerificationModal.jsx +++ b/web/src/components/common/modals/SecureVerificationModal.jsx @@ -19,7 +19,16 @@ For commercial licensing, please contact support@quantumnous.com import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Modal, Button, Input, Typography, Tabs, TabPane, Space, Spin } from '@douyinfe/semi-ui'; +import { + Modal, + Button, + Input, + Typography, + Tabs, + TabPane, + Space, + Spin, +} from '@douyinfe/semi-ui'; /** * 通用安全验证模态框组件 @@ -78,9 +87,7 @@ const SecureVerificationModal = ({ title={title || t('安全验证')} visible={visible} onCancel={onCancel} - footer={ - - } + footer={} width={500} style={{ maxWidth: '90vw' }} > @@ -123,21 +130,21 @@ const SecureVerificationModal = ({ width={460} centered style={{ - maxWidth: 'calc(100vw - 32px)' + maxWidth: 'calc(100vw - 32px)', }} bodyStyle={{ - padding: '20px 24px' + padding: '20px 24px', }} >
{/* 描述信息 */} {description && ( {description} @@ -153,10 +160,7 @@ const SecureVerificationModal = ({ style={{ margin: 0 }} > {has2FA && ( - +
- + + } style={{ width: '100%' }} @@ -178,24 +195,26 @@ const SecureVerificationModal = ({
{t('从认证器应用中获取验证码,或使用备用码')} -
+
@@ -214,31 +233,47 @@ const SecureVerificationModal = ({ )} {hasPasskey && passkeySupported && ( - +
-
-
- - +
+
+ +
- + {t('使用 Passkey 验证')} {t('点击验证按钮,使用您的生物特征或安全密钥')}
-
+
@@ -282,4 +319,4 @@ const SecureVerificationModal = ({ ); }; -export default SecureVerificationModal; \ No newline at end of file +export default SecureVerificationModal; diff --git a/web/src/components/settings/PersonalSetting.jsx b/web/src/components/settings/PersonalSetting.jsx index c9934604c..18d374801 100644 --- a/web/src/components/settings/PersonalSetting.jsx +++ b/web/src/components/settings/PersonalSetting.jsx @@ -155,9 +155,7 @@ const PersonalSetting = () => { gotifyUrl: settings.gotify_url || '', gotifyToken: settings.gotify_token || '', gotifyPriority: - settings.gotify_priority !== undefined - ? settings.gotify_priority - : 5, + settings.gotify_priority !== undefined ? settings.gotify_priority : 5, acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false, recordIpLog: settings.record_ip_log || false, @@ -214,7 +212,9 @@ const PersonalSetting = () => { return; } - const publicKey = prepareCredentialCreationOptions(data?.options || data?.publicKey || data); + const publicKey = prepareCredentialCreationOptions( + data?.options || data?.publicKey || data, + ); const credential = await navigator.credentials.create({ publicKey }); const payload = buildRegistrationResult(credential); if (!payload) { @@ -222,7 +222,10 @@ const PersonalSetting = () => { return; } - const finishRes = await API.post('/api/user/passkey/register/finish', payload); + const finishRes = await API.post( + '/api/user/passkey/register/finish', + payload, + ); if (finishRes.data.success) { showSuccess(t('Passkey 注册成功')); await loadPasskeyStatus(); diff --git a/web/src/components/settings/SystemSetting.jsx b/web/src/components/settings/SystemSetting.jsx index 780e89fb1..2f0b892ff 100644 --- a/web/src/components/settings/SystemSetting.jsx +++ b/web/src/components/settings/SystemSetting.jsx @@ -615,7 +615,10 @@ const SystemSetting = () => { options.push({ key: 'passkey.rp_display_name', - value: formValues['passkey.rp_display_name'] || inputs['passkey.rp_display_name'] || '', + value: + formValues['passkey.rp_display_name'] || + inputs['passkey.rp_display_name'] || + '', }); options.push({ key: 'passkey.rp_id', @@ -623,11 +626,17 @@ const SystemSetting = () => { }); options.push({ key: 'passkey.user_verification', - value: formValues['passkey.user_verification'] || inputs['passkey.user_verification'] || 'preferred', + value: + formValues['passkey.user_verification'] || + inputs['passkey.user_verification'] || + 'preferred', }); options.push({ key: 'passkey.attachment_preference', - value: formValues['passkey.attachment_preference'] || inputs['passkey.attachment_preference'] || '', + value: + formValues['passkey.attachment_preference'] || + inputs['passkey.attachment_preference'] || + '', }); options.push({ key: 'passkey.origins', @@ -1044,7 +1053,9 @@ const SystemSetting = () => { {t('用以支持基于 WebAuthn 的无密码登录注册')} { field="['passkey.rp_display_name']" label={t('服务显示名称')} placeholder={t('默认使用系统名称')} - extraText={t('用户注册时看到的网站名称,比如\'我的网站\'')} + extraText={t( + "用户注册时看到的网站名称,比如'我的网站'", + )} /> @@ -1078,7 +1091,9 @@ const SystemSetting = () => { field="['passkey.rp_id']" label={t('网站域名标识')} placeholder={t('例如:example.com')} - extraText={t('留空则默认使用服务器地址,注意不能携带http://或者https://')} + extraText={t( + '留空则默认使用服务器地址,注意不能携带http://或者https://', + )} /> @@ -1092,7 +1107,10 @@ const SystemSetting = () => { label={t('安全验证级别')} placeholder={t('是否要求指纹/面容等生物识别')} optionList={[ - { label: t('推荐使用(用户可选)'), value: 'preferred' }, + { + label: t('推荐使用(用户可选)'), + value: 'preferred', + }, { label: t('强制要求'), value: 'required' }, { label: t('不建议使用'), value: 'discouraged' }, ]} @@ -1109,7 +1127,9 @@ const SystemSetting = () => { { label: t('本设备内置'), value: 'platform' }, { label: t('外接设备'), value: 'cross-platform' }, ]} - extraText={t('本设备:手机指纹/面容,外接:USB安全密钥')} + extraText={t( + '本设备:手机指纹/面容,外接:USB安全密钥', + )} /> @@ -1123,7 +1143,10 @@ const SystemSetting = () => { noLabel extraText={t('仅用于开发环境,生产环境应使用 HTTPS')} onChange={(e) => - handleCheckboxChange('passkey.allow_insecure_origin', e) + handleCheckboxChange( + 'passkey.allow_insecure_origin', + e, + ) } > {t('允许不安全的 Origin(HTTP)')} @@ -1139,11 +1162,16 @@ const SystemSetting = () => { field="['passkey.origins']" label={t('允许的 Origins')} placeholder={t('填写带https的域名,逗号分隔')} - extraText={t('为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https')} + extraText={t( + '为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https', + )} /> - diff --git a/web/src/components/settings/personal/cards/AccountManagement.jsx b/web/src/components/settings/personal/cards/AccountManagement.jsx index ac2146c27..d54edb93a 100644 --- a/web/src/components/settings/personal/cards/AccountManagement.jsx +++ b/web/src/components/settings/personal/cards/AccountManagement.jsx @@ -535,7 +535,9 @@ const AccountManagement = ({ ? () => { Modal.confirm({ title: t('确认解绑 Passkey'), - content: t('解绑后将无法使用 Passkey 登录,确定要继续吗?'), + content: t( + '解绑后将无法使用 Passkey 登录,确定要继续吗?', + ), okText: t('确认解绑'), cancelText: t('取消'), okType: 'danger', @@ -547,7 +549,11 @@ const AccountManagement = ({ className={`w-full sm:w-auto ${passkeyEnabled ? '!bg-slate-500 hover:!bg-slate-600' : ''}`} icon={} disabled={!passkeySupported && !passkeyEnabled} - loading={passkeyEnabled ? passkeyDeleteLoading : passkeyRegisterLoading} + loading={ + passkeyEnabled + ? passkeyDeleteLoading + : passkeyRegisterLoading + } > {passkeyEnabled ? t('解绑 Passkey') : t('注册 Passkey')} diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index 0c99e2855..c19084a51 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -621,7 +621,9 @@ const NotificationSettings = ({ }, { pattern: /^https?:\/\/.+/, - message: t('Gotify服务器地址必须以http://或https://开头'), + message: t( + 'Gotify服务器地址必须以http://或https://开头', + ), }, ]} /> @@ -678,9 +680,7 @@ const NotificationSettings = ({ '复制应用的令牌(Token)并填写到上方的应用令牌字段', )}
-
- 3. {t('填写Gotify服务器的完整URL地址')} -
+
3. {t('填写Gotify服务器的完整URL地址')}
{t('更多信息请参考')} diff --git a/web/src/components/setup/components/steps/DatabaseStep.jsx b/web/src/components/setup/components/steps/DatabaseStep.jsx index 66923f445..d8d1d4f9f 100644 --- a/web/src/components/setup/components/steps/DatabaseStep.jsx +++ b/web/src/components/setup/components/steps/DatabaseStep.jsx @@ -26,8 +26,9 @@ import { Banner } from '@douyinfe/semi-ui'; */ const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => { // 检测是否在 Electron 环境中运行 - const isElectron = typeof window !== 'undefined' && window.electron?.isElectron; - + const isElectron = + typeof window !== 'undefined' && window.electron?.isElectron; + return ( <> {/* 数据库警告 */} diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index d5d299969..cce63340a 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -157,7 +157,7 @@ const EditChannelModal = (props) => { is_enterprise_account: false, // 字段透传控制默认值 allow_service_tier: false, - disable_store: false, // false = 允许透传(默认开启) + disable_store: false, // false = 允许透传(默认开启) allow_safety_identifier: false, }; const [batch, setBatch] = useState(false); @@ -206,7 +206,13 @@ const EditChannelModal = (props) => { channelExtraSettings: null, }); const [currentSectionIndex, setCurrentSectionIndex] = useState(0); - const formSections = ['basicInfo', 'apiConfig', 'modelConfig', 'advancedSettings', 'channelExtraSettings']; + const formSections = [ + 'basicInfo', + 'apiConfig', + 'modelConfig', + 'advancedSettings', + 'channelExtraSettings', + ]; const formContainerRef = useRef(null); // 2FA状态更新辅助函数 @@ -266,13 +272,13 @@ const EditChannelModal = (props) => { sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start', - inline: 'nearest' + inline: 'nearest', }); } }; const navigateToSection = (direction) => { - const availableSections = formSections.filter(section => { + const availableSections = formSections.filter((section) => { if (section === 'apiConfig') { return showApiConfigCard; } @@ -281,9 +287,15 @@ const EditChannelModal = (props) => { let newIndex; if (direction === 'up') { - newIndex = currentSectionIndex > 0 ? currentSectionIndex - 1 : availableSections.length - 1; + newIndex = + currentSectionIndex > 0 + ? currentSectionIndex - 1 + : availableSections.length - 1; } else { - newIndex = currentSectionIndex < availableSections.length - 1 ? currentSectionIndex + 1 : 0; + newIndex = + currentSectionIndex < availableSections.length - 1 + ? currentSectionIndex + 1 + : 0; } setCurrentSectionIndex(newIndex); @@ -509,7 +521,8 @@ const EditChannelModal = (props) => { // 读取字段透传控制设置 data.allow_service_tier = parsedSettings.allow_service_tier || false; data.disable_store = parsedSettings.disable_store || false; - data.allow_safety_identifier = parsedSettings.allow_safety_identifier || false; + data.allow_safety_identifier = + parsedSettings.allow_safety_identifier || false; } catch (error) { console.error('解析其他设置失败:', error); data.azure_responses_version = ''; @@ -686,7 +699,7 @@ const EditChannelModal = (props) => { title: t('查看渠道密钥'), description: t('为了保护账户安全,请验证您的身份。'), preferredMethod: 'passkey', // 优先使用 Passkey - } + }, ); // 如果直接返回了结果(已验证),显示密钥 @@ -990,7 +1003,8 @@ const EditChannelModal = (props) => { // 仅 OpenAI 渠道需要 store 和 safety_identifier if (localInputs.type === 1) { settings.disable_store = localInputs.disable_store === true; - settings.allow_safety_identifier = localInputs.allow_safety_identifier === true; + settings.allow_safety_identifier = + localInputs.allow_safety_identifier === true; } } @@ -1339,7 +1353,7 @@ const EditChannelModal = (props) => { padding: 0, display: 'flex', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', }} title={t('上一个表单块')} /> @@ -1355,7 +1369,7 @@ const EditChannelModal = (props) => { padding: 0, display: 'flex', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', }} title={t('下一个表单块')} /> @@ -1390,336 +1404,149 @@ const EditChannelModal = (props) => { > {() => ( -
-
formSectionRefs.current.basicInfo = el}> +
+
(formSectionRefs.current.basicInfo = el)}> {/* Header: Basic Info */}
- - - -
- - {t('基本信息')} - -
- {t('渠道的基本配置信息')} + + + +
+ + {t('基本信息')} + +
+ {t('渠道的基本配置信息')} +
-
- setChannelSearchValue(value)} - renderOptionItem={renderChannelOption} - onChange={(value) => handleInputChange('type', value)} - /> - - {inputs.type === 20 && ( - { - setIsEnterpriseAccount(value); - handleInputChange('is_enterprise_account', value); - }} - extraText={t( - '企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选', - )} - initValue={inputs.is_enterprise_account} - /> - )} - - handleInputChange('name', value)} - autoComplete='new-password' - /> - - {inputs.type === 41 && ( { - // 更新设置中的 vertex_key_type - handleChannelOtherSettingsChange( - 'vertex_key_type', - value, - ); - // 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件 - if (value === 'api_key') { - setBatch(false); - setUseManualInput(false); - setVertexKeys([]); - setVertexFileList([]); - if (formApiRef.current) { - formApiRef.current.setValue('vertex_files', []); - } - } - }} - extraText={ - inputs.vertex_key_type === 'api_key' - ? t('API Key 模式下不支持批量创建') - : t('JSON 模式支持手动输入或上传服务账号 JSON') - } + filter={selectFilter} + autoClearSearchValue={false} + searchPosition='dropdown' + onSearch={(value) => setChannelSearchValue(value)} + renderOptionItem={renderChannelOption} + onChange={(value) => handleInputChange('type', value)} /> - )} - {batch ? ( - inputs.type === 41 && - (inputs.vertex_key_type || 'json') === 'json' ? ( - } - dragMainText={t('点击上传文件或拖拽文件到这里')} - dragSubText={t('仅支持 JSON 文件,支持多文件')} - style={{ marginTop: 10 }} - uploadTrigger='custom' - beforeUpload={() => false} - onChange={handleVertexUploadChange} - fileList={vertexFileList} - rules={ - isEdit - ? [] - : [{ required: true, message: t('请上传密钥文件') }] - } - extraText={batchExtra} - /> - ) : ( - handleInputChange('key', value)} - extraText={ -
- {isEdit && - isMultiKeyChannel && - keyMode === 'append' && ( - - {t( - '追加模式:新密钥将添加到现有密钥列表的末尾', - )} - - )} - {isEdit && ( - - )} - {batchExtra} -
- } - showClear - /> - ) - ) : ( - <> - {inputs.type === 41 && - (inputs.vertex_key_type || 'json') === 'json' ? ( - <> - {!batch && ( -
- - {t('密钥输入方式')} - - - - - -
- )} - {batch && ( - - )} + {inputs.type === 20 && ( + { + setIsEnterpriseAccount(value); + handleInputChange('is_enterprise_account', value); + }} + extraText={t( + '企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选', + )} + initValue={inputs.is_enterprise_account} + /> + )} - {useManualInput && !batch ? ( - - handleInputChange('key', value) - } - extraText={ -
- - {t('请输入完整的 JSON 格式密钥内容')} - - {isEdit && - isMultiKeyChannel && - keyMode === 'append' && ( - - {t( - '追加模式:新密钥将添加到现有密钥列表的末尾', - )} - - )} - {isEdit && ( - - )} - {batchExtra} -
- } - autosize - showClear - /> - ) : ( - } - dragMainText={t('点击上传文件或拖拽文件到这里')} - dragSubText={t('仅支持 JSON 文件')} - style={{ marginTop: 10 }} - uploadTrigger='custom' - beforeUpload={() => false} - onChange={handleVertexUploadChange} - fileList={vertexFileList} - rules={ - isEdit - ? [] - : [ - { - required: true, - message: t('请上传密钥文件'), - }, - ] - } - extraText={batchExtra} - /> - )} - - ) : ( - handleInputChange('name', value)} + autoComplete='new-password' + /> + + {inputs.type === 41 && ( + { + // 更新设置中的 vertex_key_type + handleChannelOtherSettingsChange( + 'vertex_key_type', + value, + ); + // 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件 + if (value === 'api_key') { + setBatch(false); + setUseManualInput(false); + setVertexKeys([]); + setVertexFileList([]); + if (formApiRef.current) { + formApiRef.current.setValue('vertex_files', []); + } } - placeholder={t(type2secretPrompt(inputs.type))} + }} + extraText={ + inputs.vertex_key_type === 'api_key' + ? t('API Key 模式下不支持批量创建') + : t('JSON 模式支持手动输入或上传服务账号 JSON') + } + /> + )} + {batch ? ( + inputs.type === 41 && + (inputs.vertex_key_type || 'json') === 'json' ? ( + } + dragMainText={t('点击上传文件或拖拽文件到这里')} + dragSubText={t('仅支持 JSON 文件,支持多文件')} + style={{ marginTop: 10 }} + uploadTrigger='custom' + beforeUpload={() => false} + onChange={handleVertexUploadChange} + fileList={vertexFileList} + rules={ + isEdit + ? [] + : [ + { + required: true, + message: t('请上传密钥文件'), + }, + ] + } + extraText={batchExtra} + /> + ) : ( + handleInputChange('key', value)} extraText={ -
+
{isEdit && isMultiKeyChannel && keyMode === 'append' && ( @@ -1744,743 +1571,912 @@ const EditChannelModal = (props) => { } showClear /> - )} - - )} + ) + ) : ( + <> + {inputs.type === 41 && + (inputs.vertex_key_type || 'json') === 'json' ? ( + <> + {!batch && ( +
+ + {t('密钥输入方式')} + + + + + +
+ )} - {isEdit && isMultiKeyChannel && ( - setKeyMode(value)} - extraText={ - - {keyMode === 'replace' - ? t('覆盖模式:将完全替换现有的所有密钥') - : t('追加模式:将新密钥添加到现有密钥列表末尾')} - - } - /> - )} - {batch && multiToSingle && ( - <> + {batch && ( + + )} + + {useManualInput && !batch ? ( + + handleInputChange('key', value) + } + extraText={ +
+ + {t('请输入完整的 JSON 格式密钥内容')} + + {isEdit && + isMultiKeyChannel && + keyMode === 'append' && ( + + {t( + '追加模式:新密钥将添加到现有密钥列表的末尾', + )} + + )} + {isEdit && ( + + )} + {batchExtra} +
+ } + autosize + showClear + /> + ) : ( + } + dragMainText={t('点击上传文件或拖拽文件到这里')} + dragSubText={t('仅支持 JSON 文件')} + style={{ marginTop: 10 }} + uploadTrigger='custom' + beforeUpload={() => false} + onChange={handleVertexUploadChange} + fileList={vertexFileList} + rules={ + isEdit + ? [] + : [ + { + required: true, + message: t('请上传密钥文件'), + }, + ] + } + extraText={batchExtra} + /> + )} + + ) : ( + + handleInputChange('key', value) + } + extraText={ +
+ {isEdit && + isMultiKeyChannel && + keyMode === 'append' && ( + + {t( + '追加模式:新密钥将添加到现有密钥列表的末尾', + )} + + )} + {isEdit && ( + + )} + {batchExtra} +
+ } + showClear + /> + )} + + )} + + {isEdit && isMultiKeyChannel && ( { - setMultiKeyMode(value); - handleInputChange('multi_key_mode', value); - }} + value={keyMode} + onChange={(value) => setKeyMode(value)} + extraText={ + + {keyMode === 'replace' + ? t('覆盖模式:将完全替换现有的所有密钥') + : t('追加模式:将新密钥添加到现有密钥列表末尾')} + + } /> - {inputs.multi_key_mode === 'polling' && ( - + { + setMultiKeyMode(value); + handleInputChange('multi_key_mode', value); + }} /> - )} - - )} + {inputs.multi_key_mode === 'polling' && ( + + )} + + )} - {inputs.type === 18 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 18 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 41 && ( - handleInputChange('other', value)} - rules={[{ required: true, message: t('请填写部署地区') }]} - template={REGION_EXAMPLE} - templateLabel={t('填入模板')} - editorType='region' - formApi={formApiRef.current} - extraText={t('设置默认地区和特定模型的专用地区')} - /> - )} + {inputs.type === 41 && ( + handleInputChange('other', value)} + rules={[ + { required: true, message: t('请填写部署地区') }, + ]} + template={REGION_EXAMPLE} + templateLabel={t('填入模板')} + editorType='region' + formApi={formApiRef.current} + extraText={t('设置默认地区和特定模型的专用地区')} + /> + )} - {inputs.type === 21 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 21 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 39 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 39 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 49 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 49 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 1 && ( - - handleInputChange('openai_organization', value) - } - /> - )} + {inputs.type === 1 && ( + + handleInputChange('openai_organization', value) + } + /> + )}
{/* API Configuration Card */} {showApiConfigCard && ( -
formSectionRefs.current.apiConfig = el}> +
(formSectionRefs.current.apiConfig = el)}> {/* Header: API Config */}
- - - -
- - {t('API 配置')} - -
- {t('API 地址和相关配置')} + + + +
+ + {t('API 配置')} + +
+ {t('API 地址和相关配置')} +
-
- {inputs.type === 40 && ( - + {t('邀请链接')}: + + window.open( + 'https://cloud.siliconflow.cn/i/hij0YNTZ', + ) + } + > + https://cloud.siliconflow.cn/i/hij0YNTZ + +
+ } + className='!rounded-lg' + /> + )} + + {inputs.type === 3 && ( + <> +
- {t('邀请链接')}: - - window.open( - 'https://cloud.siliconflow.cn/i/hij0YNTZ', + + handleInputChange('base_url', value) + } + showClear + /> +
+
+ + handleInputChange('other', value) + } + showClear + /> +
+
+ + handleChannelOtherSettingsChange( + 'azure_responses_version', + value, ) } - > - https://cloud.siliconflow.cn/i/hij0YNTZ - + showClear + />
- } - className='!rounded-lg' - /> - )} + + )} - {inputs.type === 3 && ( - <> + {inputs.type === 8 && ( + <> + +
+ + handleInputChange('base_url', value) + } + showClear + /> +
+ + )} + + {inputs.type === 37 && ( -
- - handleInputChange('base_url', value) - } - showClear - /> -
-
- - handleInputChange('other', value) - } - showClear - /> -
-
- - handleChannelOtherSettingsChange( - 'azure_responses_version', - value, - ) - } - showClear - /> -
- - )} + )} - {inputs.type === 8 && ( - <> - -
- - handleInputChange('base_url', value) - } - showClear - /> -
- - )} - - {inputs.type === 37 && ( - + + handleInputChange('base_url', value) + } + showClear + extraText={t( + '对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写', + )} + /> +
)} - className='!rounded-lg' - /> - )} - {inputs.type !== 3 && - inputs.type !== 8 && - inputs.type !== 22 && - inputs.type !== 36 && - inputs.type !== 45 && ( + {inputs.type === 22 && (
handleInputChange('base_url', value) } showClear - extraText={t( - '对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写', - )} />
)} - {inputs.type === 22 && ( -
- - handleInputChange('base_url', value) - } - showClear - /> -
- )} + {inputs.type === 36 && ( +
+ + handleInputChange('base_url', value) + } + showClear + /> +
+ )} - {inputs.type === 36 && ( -
- - handleInputChange('base_url', value) - } - showClear - /> -
- )} - - {inputs.type === 45 && ( -
- - handleInputChange('base_url', value) - } - optionList={[ - { - value: 'https://ark.cn-beijing.volces.com', - label: 'https://ark.cn-beijing.volces.com', - }, - { - value: 'https://ark.ap-southeast.bytepluses.com', - label: 'https://ark.ap-southeast.bytepluses.com', - }, - ]} - defaultValue='https://ark.cn-beijing.volces.com' - /> -
- )} + {inputs.type === 45 && ( +
+ + handleInputChange('base_url', value) + } + optionList={[ + { + value: 'https://ark.cn-beijing.volces.com', + label: 'https://ark.cn-beijing.volces.com', + }, + { + value: + 'https://ark.ap-southeast.bytepluses.com', + label: + 'https://ark.ap-southeast.bytepluses.com', + }, + ]} + defaultValue='https://ark.cn-beijing.volces.com' + /> +
+ )}
)} {/* Model Configuration Card */} -
formSectionRefs.current.modelConfig = el}> +
(formSectionRefs.current.modelConfig = el)}> {/* Header: Model Config */}
- - - -
- - {t('模型配置')} - -
- {t('模型选择和映射设置')} + + + +
+ + {t('模型配置')} + +
+ {t('模型选择和映射设置')} +
-
- handleInputChange('models', value)} - renderSelectedItem={(optionNode) => { - const modelName = String(optionNode?.value ?? ''); - return { - isRenderInTag: true, - content: ( - { - e.stopPropagation(); - const ok = await copy(modelName); - if (ok) { - showSuccess( - t('已复制:{{name}}', { name: modelName }), - ); - } else { + handleInputChange('models', value)} + renderSelectedItem={(optionNode) => { + const modelName = String(optionNode?.value ?? ''); + return { + isRenderInTag: true, + content: ( + { + e.stopPropagation(); + const ok = await copy(modelName); + if (ok) { + showSuccess( + t('已复制:{{name}}', { name: modelName }), + ); + } else { + showError(t('复制失败')); + } + }} + > + {optionNode.label || modelName} + + ), + }; + }} + extraText={ + + + + {MODEL_FETCHABLE_TYPES.has(inputs.type) && ( + + )} + + + {modelGroups && + modelGroups.length > 0 && + modelGroups.map((group) => ( + + ))} + + } + /> + + setCustomModel(value.trim())} + value={customModel} + suffix={ - - {MODEL_FETCHABLE_TYPES.has(inputs.type) && ( - - )} - - - {modelGroups && - modelGroups.length > 0 && - modelGroups.map((group) => ( - - ))} - - } - /> + } + /> - setCustomModel(value.trim())} - value={customModel} - suffix={ - - } - /> + + handleInputChange('test_model', value) + } + showClear + /> - handleInputChange('test_model', value)} - showClear - /> - - - handleInputChange('model_mapping', value) - } - template={MODEL_MAPPING_EXAMPLE} - templateLabel={t('填入模板')} - editorType='keyValue' - formApi={formApiRef.current} - extraText={t('键为请求中的模型名称,值为要替换的模型名称')} - /> + + handleInputChange('model_mapping', value) + } + template={MODEL_MAPPING_EXAMPLE} + templateLabel={t('填入模板')} + editorType='keyValue' + formApi={formApiRef.current} + extraText={t( + '键为请求中的模型名称,值为要替换的模型名称', + )} + />
{/* Advanced Settings Card */} -
formSectionRefs.current.advancedSettings = el}> +
(formSectionRefs.current.advancedSettings = el)} + > {/* Header: Advanced Settings */}
- - - -
- - {t('高级设置')} - -
- {t('渠道的高级配置选项')} + + + +
+ + {t('高级设置')} + +
+ {t('渠道的高级配置选项')} +
-
- handleInputChange('groups', value)} - /> + handleInputChange('groups', value)} + /> - handleInputChange('tag', value)} - /> - handleInputChange('remark', value)} - /> + handleInputChange('tag', value)} + /> + handleInputChange('remark', value)} + /> - - - - handleInputChange('priority', value) - } - style={{ width: '100%' }} - /> - - - - handleInputChange('weight', value) - } - style={{ width: '100%' }} - /> - - - - setAutoBan(value)} - extraText={t( - '仅当自动禁用开启时有效,关闭后不会自动禁用该渠道', - )} - initValue={autoBan} - /> - - - handleInputChange('param_override', value) - } - extraText={ -
- - handleInputChange( - 'param_override', - JSON.stringify({ temperature: 0 }, null, 2), - ) + + + + handleInputChange('priority', value) } - > - {t('旧格式模板')} - - - handleInputChange( - 'param_override', - JSON.stringify( - { - operations: [ - { - path: 'temperature', - mode: 'set', - value: 0.7, - conditions: [ - { - path: 'model', - mode: 'prefix', - value: 'gpt', - }, - ], - logic: 'AND', - }, - ], - }, - null, - 2, - ), - ) + style={{ width: '100%' }} + /> + + + + handleInputChange('weight', value) } - > - {t('新格式模板')} - -
- } - showClear - /> + style={{ width: '100%' }} + /> + + - - handleInputChange('header_override', value) - } - extraText={ + setAutoBan(value)} + extraText={t( + '仅当自动禁用开启时有效,关闭后不会自动禁用该渠道', + )} + initValue={autoBan} + /> -
-
+ + handleInputChange('param_override', value) + } + extraText={ +
handleInputChange( - 'header_override', + 'param_override', + JSON.stringify({ temperature: 0 }, null, 2), + ) + } + > + {t('旧格式模板')} + + + handleInputChange( + 'param_override', JSON.stringify( { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', - 'Authorization': 'Bearer{api_key}', + operations: [ + { + path: 'temperature', + mode: 'set', + value: 0.7, + conditions: [ + { + path: 'model', + mode: 'prefix', + value: 'gpt', + }, + ], + logic: 'AND', + }, + ], }, null, 2, @@ -2488,220 +2484,281 @@ const EditChannelModal = (props) => { ) } > - {t('填入模板')} + {t('新格式模板')}
-
- - {t('支持变量:')} - -
-
{t('渠道密钥')}: {'{api_key}'}
+ } + showClear + /> + + + handleInputChange('header_override', value) + } + extraText={ +
+
+ + handleInputChange( + 'header_override', + JSON.stringify( + { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', + Authorization: 'Bearer{api_key}', + }, + null, + 2, + ), + ) + } + > + {t('填入模板')} + +
+
+ + {t('支持变量:')} + +
+
+ {t('渠道密钥')}: {'{api_key}'} +
+
-
- } - showClear - /> + } + showClear + /> - - handleInputChange('status_code_mapping', value) - } - template={STATUS_CODE_MAPPING_EXAMPLE} - templateLabel={t('填入模板')} - editorType='keyValue' - formApi={formApiRef.current} - extraText={t( - '键为原状态码,值为要复写的状态码,仅影响本地判断', + + handleInputChange('status_code_mapping', value) + } + template={STATUS_CODE_MAPPING_EXAMPLE} + templateLabel={t('填入模板')} + editorType='keyValue' + formApi={formApiRef.current} + extraText={t( + '键为原状态码,值为要复写的状态码,仅影响本地判断', + )} + /> + + {/* 字段透传控制 - OpenAI 渠道 */} + {inputs.type === 1 && ( + <> +
+ {t('字段透传控制')} +
+ + + handleChannelOtherSettingsChange( + 'allow_service_tier', + value, + ) + } + extraText={t( + 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', + )} + /> + + + handleChannelOtherSettingsChange( + 'disable_store', + value, + ) + } + extraText={t( + 'store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用', + )} + /> + + + handleChannelOtherSettingsChange( + 'allow_safety_identifier', + value, + ) + } + extraText={t( + 'safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私', + )} + /> + )} - /> - {/* 字段透传控制 - OpenAI 渠道 */} - {inputs.type === 1 && ( - <> -
- {t('字段透传控制')} -
+ {/* 字段透传控制 - Claude 渠道 */} + {inputs.type === 14 && ( + <> +
+ {t('字段透传控制')} +
- - handleChannelOtherSettingsChange('allow_service_tier', value) - } - extraText={t( - 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', - )} - /> - - - handleChannelOtherSettingsChange('disable_store', value) - } - extraText={t( - 'store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用', - )} - /> - - - handleChannelOtherSettingsChange('allow_safety_identifier', value) - } - extraText={t( - 'safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私', - )} - /> - - )} - - {/* 字段透传控制 - Claude 渠道 */} - {(inputs.type === 14) && ( - <> -
- {t('字段透传控制')} -
- - - handleChannelOtherSettingsChange('allow_service_tier', value) - } - extraText={t( - 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', - )} - /> - - )} + + handleChannelOtherSettingsChange( + 'allow_service_tier', + value, + ) + } + extraText={t( + 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', + )} + /> + + )}
{/* Channel Extra Settings Card */} -
formSectionRefs.current.channelExtraSettings = el}> +
+ (formSectionRefs.current.channelExtraSettings = el) + } + > {/* Header: Channel Extra Settings */}
- - - -
- - {t('渠道额外设置')} - + + + +
+ + {t('渠道额外设置')} + +
-
- {inputs.type === 1 && ( + {inputs.type === 1 && ( + + handleChannelSettingsChange('force_format', value) + } + extraText={t( + '强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)', + )} + /> + )} + - handleChannelSettingsChange('force_format', value) + handleChannelSettingsChange( + 'thinking_to_content', + value, + ) } extraText={t( - '强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)', + '将 reasoning_content 转换为 标签拼接到内容中', )} /> - )} - - handleChannelSettingsChange('thinking_to_content', value) - } - extraText={t( - '将 reasoning_content 转换为 标签拼接到内容中', - )} - /> + + handleChannelSettingsChange( + 'pass_through_body_enabled', + value, + ) + } + extraText={t('启用请求体透传功能')} + /> - - handleChannelSettingsChange( - 'pass_through_body_enabled', - value, - ) - } - extraText={t('启用请求体透传功能')} - /> + + handleChannelSettingsChange('proxy', value) + } + showClear + extraText={t('用于配置网络代理,支持 socks5 协议')} + /> - - handleChannelSettingsChange('proxy', value) - } - showClear - extraText={t('用于配置网络代理,支持 socks5 协议')} - /> - - - handleChannelSettingsChange('system_prompt', value) - } - autosize - showClear - extraText={t( - '用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置', - )} - /> - - handleChannelSettingsChange( - 'system_prompt_override', - value, - ) - } - extraText={t( - '如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面', - )} - /> + + handleChannelSettingsChange('system_prompt', value) + } + autosize + showClear + extraText={t( + '用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置', + )} + /> + + handleChannelSettingsChange( + 'system_prompt_override', + value, + ) + } + extraText={t( + '如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面', + )} + />
diff --git a/web/src/components/table/channels/modals/EditTagModal.jsx b/web/src/components/table/channels/modals/EditTagModal.jsx index 752ff3dc5..0b2f17d90 100644 --- a/web/src/components/table/channels/modals/EditTagModal.jsx +++ b/web/src/components/table/channels/modals/EditTagModal.jsx @@ -119,8 +119,19 @@ const EditTagModal = (props) => { localModels = ['suno_music', 'suno_lyrics']; break; case 53: - localModels = ['NousResearch/Hermes-4-405B-FP8', 'Qwen/Qwen3-235B-A22B-Thinking-2507', 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8','Qwen/Qwen3-235B-A22B-Instruct-2507', 'zai-org/GLM-4.5-FP8', 'openai/gpt-oss-120b', 'deepseek-ai/DeepSeek-R1-0528', 'deepseek-ai/DeepSeek-R1', 'deepseek-ai/DeepSeek-V3-0324', 'deepseek-ai/DeepSeek-V3.1']; - break; + localModels = [ + 'NousResearch/Hermes-4-405B-FP8', + 'Qwen/Qwen3-235B-A22B-Thinking-2507', + 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8', + 'Qwen/Qwen3-235B-A22B-Instruct-2507', + 'zai-org/GLM-4.5-FP8', + 'openai/gpt-oss-120b', + 'deepseek-ai/DeepSeek-R1-0528', + 'deepseek-ai/DeepSeek-R1', + 'deepseek-ai/DeepSeek-V3-0324', + 'deepseek-ai/DeepSeek-V3.1', + ]; + break; default: localModels = getChannelModels(value); break; diff --git a/web/src/components/table/channels/modals/ModelTestModal.jsx b/web/src/components/table/channels/modals/ModelTestModal.jsx index 7cc56612d..1879cd574 100644 --- a/web/src/components/table/channels/modals/ModelTestModal.jsx +++ b/web/src/components/table/channels/modals/ModelTestModal.jsx @@ -67,9 +67,15 @@ const ModelTestModal = ({ { value: 'openai', label: 'OpenAI (/v1/chat/completions)' }, { value: 'openai-response', label: 'OpenAI Response (/v1/responses)' }, { value: 'anthropic', label: 'Anthropic (/v1/messages)' }, - { value: 'gemini', label: 'Gemini (/v1beta/models/{model}:generateContent)' }, + { + value: 'gemini', + label: 'Gemini (/v1beta/models/{model}:generateContent)', + }, { value: 'jina-rerank', label: 'Jina Rerank (/rerank)' }, - { value: 'image-generation', label: t('图像生成') + ' (/v1/images/generations)' }, + { + value: 'image-generation', + label: t('图像生成') + ' (/v1/images/generations)', + }, { value: 'embeddings', label: 'Embeddings (/v1/embeddings)' }, ]; @@ -166,7 +172,13 @@ const ModelTestModal = ({ return ( - -
@@ -339,16 +356,22 @@ const RechargeCard = ({ )} {(enableOnlineTopUp || enableStripeTopUp) && ( - {t('选择充值额度')} {(() => { const { symbol, rate, type } = getCurrencyConfig(); if (type === 'USD') return null; - + return ( - + (1 $ = {rate.toFixed(2)} {symbol}) ); @@ -378,11 +401,11 @@ const RechargeCard = ({ usdRate = s?.usd_exchange_rate || 7; } } catch (e) {} - + let displayValue = preset.value; // 显示的数量 let displayActualPay = actualPay; let displaySave = save; - + if (type === 'USD') { // 数量保持USD,价格从CNY转USD displayActualPay = actualPay / usdRate; @@ -444,7 +467,8 @@ const RechargeCard = ({ margin: '4px 0', }} > - {t('实付')} {symbol}{displayActualPay.toFixed(2)}, + {t('实付')} {symbol} + {displayActualPay.toFixed(2)}, {hasDiscount ? `${t('节省')} ${symbol}${displaySave.toFixed(2)}` : `${t('节省')} ${symbol}0.00`} diff --git a/web/src/components/topup/index.jsx b/web/src/components/topup/index.jsx index 558c67050..9054da524 100644 --- a/web/src/components/topup/index.jsx +++ b/web/src/components/topup/index.jsx @@ -37,6 +37,7 @@ import RechargeCard from './RechargeCard'; import InvitationCard from './InvitationCard'; import TransferModal from './modals/TransferModal'; import PaymentConfirmModal from './modals/PaymentConfirmModal'; +import TopupHistoryModal from './modals/TopupHistoryModal'; const TopUp = () => { const { t } = useTranslation(); @@ -77,6 +78,9 @@ const TopUp = () => { const [openTransfer, setOpenTransfer] = useState(false); const [transferAmount, setTransferAmount] = useState(0); + // 账单Modal状态 + const [openHistory, setOpenHistory] = useState(false); + // 预设充值额度选项 const [presetAmounts, setPresetAmounts] = useState([]); const [selectedPreset, setSelectedPreset] = useState(null); @@ -488,6 +492,14 @@ const TopUp = () => { setOpenTransfer(false); }; + const handleOpenHistory = () => { + setOpenHistory(true); + }; + + const handleHistoryCancel = () => { + setOpenHistory(false); + }; + // 选择预设充值额度 const selectPresetAmount = (preset) => { setTopUpCount(preset.value); @@ -544,6 +556,13 @@ const TopUp = () => { discountRate={topupInfo?.discount?.[topUpCount] || 1.0} /> + {/* 充值账单模态框 */} + + {/* 用户信息头部 */}
@@ -580,6 +599,7 @@ const TopUp = () => { renderQuota={renderQuota} statusLoading={statusLoading} topupInfo={topupInfo} + onOpenHistory={handleOpenHistory} />
diff --git a/web/src/components/topup/modals/TopupHistoryModal.jsx b/web/src/components/topup/modals/TopupHistoryModal.jsx new file mode 100644 index 000000000..57916a9aa --- /dev/null +++ b/web/src/components/topup/modals/TopupHistoryModal.jsx @@ -0,0 +1,268 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ +import React, { useState, useEffect, useMemo } from 'react'; +import { + Modal, + Table, + Badge, + Typography, + Toast, + Empty, + Button, + Input, +} from '@douyinfe/semi-ui'; +import { + IllustrationNoResult, + IllustrationNoResultDark, +} from '@douyinfe/semi-illustrations'; +import { Coins } from 'lucide-react'; +import { IconSearch } from '@douyinfe/semi-icons'; +import { API, timestamp2string } from '../../../helpers'; +import { isAdmin } from '../../../helpers/utils'; +import { useIsMobile } from '../../../hooks/common/useIsMobile'; + +const { Text } = Typography; + +// 状态映射配置 +const STATUS_CONFIG = { + success: { type: 'success', key: '成功' }, + pending: { type: 'warning', key: '待支付' }, + expired: { type: 'danger', key: '已过期' }, +}; + +// 支付方式映射 +const PAYMENT_METHOD_MAP = { + stripe: 'Stripe', + alipay: '支付宝', + wxpay: '微信', +}; + +const TopupHistoryModal = ({ visible, onCancel, t }) => { + const [loading, setLoading] = useState(false); + const [topups, setTopups] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [keyword, setKeyword] = useState(''); + + const isMobile = useIsMobile(); + + const loadTopups = async (currentPage, currentPageSize) => { + setLoading(true); + try { + const base = isAdmin() ? '/api/user/topup' : '/api/user/topup/self'; + const qs = + `p=${currentPage}&page_size=${currentPageSize}` + + (keyword ? `&keyword=${encodeURIComponent(keyword)}` : ''); + const endpoint = `${base}?${qs}`; + const res = await API.get(endpoint); + const { success, message, data } = res.data; + if (success) { + setTopups(data.items || []); + setTotal(data.total || 0); + } else { + Toast.error({ content: message || t('加载失败') }); + } + } catch (error) { + console.error('Load topups error:', error); + Toast.error({ content: t('加载账单失败') }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (visible) { + loadTopups(page, pageSize); + } + }, [visible, page, pageSize, keyword]); + + const handlePageChange = (currentPage) => { + setPage(currentPage); + }; + + const handlePageSizeChange = (currentPageSize) => { + setPageSize(currentPageSize); + setPage(1); + }; + + // 管理员补单 + const handleAdminComplete = async (tradeNo) => { + try { + const res = await API.post('/api/user/topup/complete', { + trade_no: tradeNo, + }); + const { success, message } = res.data; + if (success) { + Toast.success({ content: t('补单成功') }); + await loadTopups(page, pageSize); + } else { + Toast.error({ content: message || t('补单失败') }); + } + } catch (e) { + Toast.error({ content: t('补单失败') }); + } + }; + + const confirmAdminComplete = (tradeNo) => { + Modal.confirm({ + title: t('确认补单'), + content: t('是否将该订单标记为成功并为用户入账?'), + onOk: () => handleAdminComplete(tradeNo), + }); + }; + + // 渲染状态徽章 + const renderStatusBadge = (status) => { + const config = STATUS_CONFIG[status] || { type: 'primary', key: status }; + return ( + + + {t(config.key)} + + ); + }; + + // 渲染支付方式 + const renderPaymentMethod = (pm) => { + const displayName = PAYMENT_METHOD_MAP[pm]; + return {displayName ? t(displayName) : pm || '-'}; + }; + + // 检查是否为管理员 + const userIsAdmin = useMemo(() => isAdmin(), []); + + const columns = useMemo(() => { + const baseColumns = [ + { + title: t('订单号'), + dataIndex: 'trade_no', + key: 'trade_no', + render: (text) => {text}, + }, + { + title: t('支付方式'), + dataIndex: 'payment_method', + key: 'payment_method', + render: renderPaymentMethod, + }, + { + title: t('充值额度'), + dataIndex: 'amount', + key: 'amount', + render: (amount) => ( + + + {amount} + + ), + }, + { + title: t('支付金额'), + dataIndex: 'money', + key: 'money', + render: (money) => ¥{money.toFixed(2)}, + }, + { + title: t('状态'), + dataIndex: 'status', + key: 'status', + render: renderStatusBadge, + }, + ]; + + // 管理员才显示操作列 + if (userIsAdmin) { + baseColumns.push({ + title: t('操作'), + key: 'action', + render: (_, record) => { + if (record.status !== 'pending') return null; + return ( + + ); + }, + }); + } + + baseColumns.push({ + title: t('创建时间'), + dataIndex: 'create_time', + key: 'create_time', + render: (time) => timestamp2string(time), + }); + + return baseColumns; + }, [t, userIsAdmin]); + + return ( + +
+ } + placeholder={t('订单号')} + value={keyword} + onChange={setKeyword} + showClear + /> +
+ } + darkModeImage={ + + } + description={t('暂无充值记录')} + style={{ padding: 30 }} + /> + } + /> + + ); +}; + +export default TopupHistoryModal; diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 3b376ed35..ad6999365 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -159,7 +159,7 @@ export const CHANNEL_OPTIONS = [ color: 'purple', label: 'Vidu', }, - { + { value: 53, color: 'blue', label: 'SubModel', diff --git a/web/src/helpers/passkey.js b/web/src/helpers/passkey.js index ae62775e8..c3f3e927d 100644 --- a/web/src/helpers/passkey.js +++ b/web/src/helpers/passkey.js @@ -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 . + +For commercial licensing, please contact support@quantumnous.com +*/ export function base64UrlToBuffer(base64url) { if (!base64url) return new ArrayBuffer(0); let padding = '='.repeat((4 - (base64url.length % 4)) % 4); @@ -26,7 +44,11 @@ export function bufferToBase64Url(buffer) { } export function prepareCredentialCreationOptions(payload) { - const options = payload?.publicKey || payload?.PublicKey || payload?.response || payload?.Response; + const options = + payload?.publicKey || + payload?.PublicKey || + payload?.response || + payload?.Response; if (!options) { throw new Error('无法从服务端响应中解析 Passkey 注册参数'); } @@ -46,7 +68,10 @@ export function prepareCredentialCreationOptions(payload) { })); } - if (Array.isArray(options.attestationFormats) && options.attestationFormats.length === 0) { + if ( + Array.isArray(options.attestationFormats) && + options.attestationFormats.length === 0 + ) { delete publicKey.attestationFormats; } @@ -54,7 +79,11 @@ export function prepareCredentialCreationOptions(payload) { } export function prepareCredentialRequestOptions(payload) { - const options = payload?.publicKey || payload?.PublicKey || payload?.response || payload?.Response; + const options = + payload?.publicKey || + payload?.PublicKey || + payload?.response || + payload?.Response; if (!options) { throw new Error('无法从服务端响应中解析 Passkey 登录参数'); } @@ -77,7 +106,10 @@ export function buildRegistrationResult(credential) { if (!credential) return null; const { response } = credential; - const transports = typeof response.getTransports === 'function' ? response.getTransports() : undefined; + const transports = + typeof response.getTransports === 'function' + ? response.getTransports() + : undefined; return { id: credential.id, @@ -107,7 +139,9 @@ export function buildAssertionResult(assertion) { authenticatorData: bufferToBase64Url(response.authenticatorData), clientDataJSON: bufferToBase64Url(response.clientDataJSON), signature: bufferToBase64Url(response.signature), - userHandle: response.userHandle ? bufferToBase64Url(response.userHandle) : null, + userHandle: response.userHandle + ? bufferToBase64Url(response.userHandle) + : null, }, clientExtensionResults: assertion.getClientExtensionResults?.() ?? {}, }; @@ -117,15 +151,22 @@ export async function isPasskeySupported() { if (typeof window === 'undefined' || !window.PublicKeyCredential) { return false; } - if (typeof window.PublicKeyCredential.isConditionalMediationAvailable === 'function') { + if ( + typeof window.PublicKeyCredential.isConditionalMediationAvailable === + 'function' + ) { try { - const available = await window.PublicKeyCredential.isConditionalMediationAvailable(); + const available = + await window.PublicKeyCredential.isConditionalMediationAvailable(); if (available) return true; } catch (error) { // ignore } } - if (typeof window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function') { + if ( + typeof window.PublicKeyCredential + .isUserVerifyingPlatformAuthenticatorAvailable === 'function' + ) { try { return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); } catch (error) { @@ -134,4 +175,3 @@ export async function isPasskeySupported() { } return true; } - diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx index b5fcb4d62..c22bdd782 100644 --- a/web/src/helpers/render.jsx +++ b/web/src/helpers/render.jsx @@ -929,10 +929,10 @@ export function renderQuotaWithAmount(amount) { export function getCurrencyConfig() { const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; const statusStr = localStorage.getItem('status'); - + let symbol = '$'; let rate = 1; - + if (quotaDisplayType === 'CNY') { symbol = '¥'; try { @@ -950,7 +950,7 @@ export function getCurrencyConfig() { } } catch (e) {} } - + return { symbol, rate, type: quotaDisplayType }; } @@ -1128,7 +1128,7 @@ export function renderModelPrice( user_group_ratio, ); groupRatio = effectiveGroupRatio; - + // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); @@ -1177,13 +1177,16 @@ export function renderModelPrice( <>

- {i18next.t('输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}', { - symbol: symbol, - price: (inputRatioPrice * rate).toFixed(6), - audioPrice: audioInputSeperatePrice - ? `,音频 ${symbol}${(audioInputPrice * rate).toFixed(6)} / 1M tokens` - : '', - })} + {i18next.t( + '输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}', + { + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + audioPrice: audioInputSeperatePrice + ? `,音频 ${symbol}${(audioInputPrice * rate).toFixed(6)} / 1M tokens` + : '', + }, + )}

{i18next.t( @@ -1311,27 +1314,27 @@ export function renderModelPrice( const extraServices = [ webSearch && webSearchCallCount > 0 ? i18next.t( - ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', - { - count: webSearchCallCount, - symbol: symbol, - price: (webSearchPrice * rate).toFixed(6), - ratio: groupRatio, - ratioType: ratioLabel, - }, - ) + ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', + { + count: webSearchCallCount, + symbol: symbol, + price: (webSearchPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + }, + ) : '', fileSearch && fileSearchCallCount > 0 ? i18next.t( - ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', - { - count: fileSearchCallCount, - symbol: symbol, - price: (fileSearchPrice * rate).toFixed(6), - ratio: groupRatio, - ratioType: ratioLabel, - }, - ) + ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', + { + count: fileSearchCallCount, + symbol: symbol, + price: (fileSearchPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + }, + ) : '', imageGenerationCall && imageGenerationCallPrice > 0 ? i18next.t( @@ -1384,7 +1387,7 @@ export function renderLogContent( label: ratioLabel, useUserGroupRatio: useUserGroupRatio, } = getEffectiveRatio(groupRatio, user_group_ratio); - + // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); @@ -1484,10 +1487,10 @@ export function renderAudioModelPrice( user_group_ratio, ); groupRatio = effectiveGroupRatio; - + // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); - + // 1 ratio = $0.002 / 1K tokens if (modelPrice !== -1) { return i18next.t( @@ -1522,10 +1525,10 @@ export function renderAudioModelPrice( let audioPrice = (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio + (audioCompletionTokens / 1000000) * - inputRatioPrice * - audioRatio * - audioCompletionRatio * - groupRatio; + inputRatioPrice * + audioRatio * + audioCompletionRatio * + groupRatio; let price = textPrice + audioPrice; return ( <> @@ -1577,7 +1580,12 @@ export function renderAudioModelPrice( { symbol: symbol, price: (inputRatioPrice * rate).toFixed(6), - total: (inputRatioPrice * audioRatio * audioCompletionRatio * rate).toFixed(6), + total: ( + inputRatioPrice * + audioRatio * + audioCompletionRatio * + rate + ).toFixed(6), audioRatio: audioRatio, audioCompRatio: audioCompletionRatio, }, @@ -1586,29 +1594,31 @@ export function renderAudioModelPrice(

{cacheTokens > 0 ? i18next.t( - '文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', - { - nonCacheInput: inputTokens - cacheTokens, - cacheInput: cacheTokens, - symbol: symbol, - cachePrice: (inputRatioPrice * cacheRatio * rate).toFixed(6), - price: (inputRatioPrice * rate).toFixed(6), - completion: completionTokens, - compPrice: (completionRatioPrice * rate).toFixed(6), - total: (textPrice * rate).toFixed(6), - }, - ) + '文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', + { + nonCacheInput: inputTokens - cacheTokens, + cacheInput: cacheTokens, + symbol: symbol, + cachePrice: (inputRatioPrice * cacheRatio * rate).toFixed( + 6, + ), + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + compPrice: (completionRatioPrice * rate).toFixed(6), + total: (textPrice * rate).toFixed(6), + }, + ) : i18next.t( - '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', - { - input: inputTokens, - symbol: symbol, - price: (inputRatioPrice * rate).toFixed(6), - completion: completionTokens, - compPrice: (completionRatioPrice * rate).toFixed(6), - total: (textPrice * rate).toFixed(6), - }, - )} + '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', + { + input: inputTokens, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + compPrice: (completionRatioPrice * rate).toFixed(6), + total: (textPrice * rate).toFixed(6), + }, + )}

{i18next.t( @@ -1617,9 +1627,15 @@ export function renderAudioModelPrice( input: audioInputTokens, completion: audioCompletionTokens, symbol: symbol, - audioInputPrice: (audioRatio * inputRatioPrice * rate).toFixed(6), - audioCompPrice: - (audioRatio * audioCompletionRatio * inputRatioPrice * rate).toFixed(6), + audioInputPrice: (audioRatio * inputRatioPrice * rate).toFixed( + 6, + ), + audioCompPrice: ( + audioRatio * + audioCompletionRatio * + inputRatioPrice * + rate + ).toFixed(6), total: (audioPrice * rate).toFixed(6), }, )} @@ -1668,7 +1684,7 @@ export function renderClaudeModelPrice( user_group_ratio, ); groupRatio = effectiveGroupRatio; - + // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); @@ -1757,37 +1773,39 @@ export function renderClaudeModelPrice(

{cacheTokens > 0 || cacheCreationTokens > 0 ? i18next.t( - '提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', - { - nonCacheInput: nonCachedTokens, - cacheInput: cacheTokens, - cacheRatio: cacheRatio, - cacheCreationInput: cacheCreationTokens, - cacheCreationRatio: cacheCreationRatio, - symbol: symbol, - cachePrice: (cacheRatioPrice * rate).toFixed(2), - cacheCreationPrice: (cacheCreationRatioPrice * rate).toFixed(6), - price: (inputRatioPrice * rate).toFixed(6), - completion: completionTokens, - compPrice: (completionRatioPrice * rate).toFixed(6), - ratio: groupRatio, - ratioType: ratioLabel, - total: (price * rate).toFixed(6), - }, - ) + '提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', + { + nonCacheInput: nonCachedTokens, + cacheInput: cacheTokens, + cacheRatio: cacheRatio, + cacheCreationInput: cacheCreationTokens, + cacheCreationRatio: cacheCreationRatio, + symbol: symbol, + cachePrice: (cacheRatioPrice * rate).toFixed(2), + cacheCreationPrice: ( + cacheCreationRatioPrice * rate + ).toFixed(6), + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + compPrice: (completionRatioPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + total: (price * rate).toFixed(6), + }, + ) : i18next.t( - '提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', - { - input: inputTokens, - symbol: symbol, - price: (inputRatioPrice * rate).toFixed(6), - completion: completionTokens, - compPrice: (completionRatioPrice * rate).toFixed(6), - ratio: groupRatio, - ratioType: ratioLabel, - total: (price * rate).toFixed(6), - }, - )} + '提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', + { + input: inputTokens, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + compPrice: (completionRatioPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + total: (price * rate).toFixed(6), + }, + )}

{i18next.t('仅供参考,以实际扣费为准')}

@@ -1810,7 +1828,7 @@ export function renderClaudeLogContent( user_group_ratio, ); groupRatio = effectiveGroupRatio; - + // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); diff --git a/web/src/helpers/secureApiCall.js b/web/src/helpers/secureApiCall.js index b82a6ae92..0054e04ae 100644 --- a/web/src/helpers/secureApiCall.js +++ b/web/src/helpers/secureApiCall.js @@ -37,7 +37,7 @@ export function isVerificationRequiredError(error) { const verificationCodes = [ 'VERIFICATION_REQUIRED', 'VERIFICATION_EXPIRED', - 'VERIFICATION_INVALID' + 'VERIFICATION_INVALID', ]; return verificationCodes.includes(data.code); @@ -57,6 +57,6 @@ export function extractVerificationInfo(error) { return { code: data.code, message: data.message || '需要安全验证', - required: true + required: true, }; -} \ No newline at end of file +} diff --git a/web/src/hooks/channels/useChannelsData.jsx b/web/src/hooks/channels/useChannelsData.jsx index 109d2e0f8..f3f99f01e 100644 --- a/web/src/hooks/channels/useChannelsData.jsx +++ b/web/src/hooks/channels/useChannelsData.jsx @@ -84,7 +84,7 @@ export const useChannelsData = () => { const [selectedModelKeys, setSelectedModelKeys] = useState([]); const [isBatchTesting, setIsBatchTesting] = useState(false); const [modelTablePage, setModelTablePage] = useState(1); -const [selectedEndpointType, setSelectedEndpointType] = useState(''); + const [selectedEndpointType, setSelectedEndpointType] = useState(''); // 使用 ref 来避免闭包问题,类似旧版实现 const shouldStopBatchTestingRef = useRef(false); diff --git a/web/src/hooks/common/useSecureVerification.jsx b/web/src/hooks/common/useSecureVerification.jsx index e60a104db..9109ec7d7 100644 --- a/web/src/hooks/common/useSecureVerification.jsx +++ b/web/src/hooks/common/useSecureVerification.jsx @@ -31,11 +31,11 @@ import { isVerificationRequiredError } from '../../helpers/secureApiCall'; * @param {string} options.successMessage - 成功提示消息 * @param {boolean} options.autoReset - 验证完成后是否自动重置状态,默认为 true */ -export const useSecureVerification = ({ - onSuccess, - onError, +export const useSecureVerification = ({ + onSuccess, + onError, successMessage, - autoReset = true + autoReset = true, } = {}) => { const { t } = useTranslation(); @@ -43,7 +43,7 @@ export const useSecureVerification = ({ const [verificationMethods, setVerificationMethods] = useState({ has2FA: false, hasPasskey: false, - passkeySupported: false + passkeySupported: false, }); // 模态框状态 @@ -54,12 +54,13 @@ export const useSecureVerification = ({ method: null, // '2fa' | 'passkey' loading: false, code: '', - apiCall: null + apiCall: null, }); // 检查可用的验证方式 const checkVerificationMethods = useCallback(async () => { - const methods = await SecureVerificationService.checkAvailableVerificationMethods(); + const methods = + await SecureVerificationService.checkAvailableVerificationMethods(); setVerificationMethods(methods); return methods; }, []); @@ -75,94 +76,108 @@ export const useSecureVerification = ({ method: null, loading: false, code: '', - apiCall: null + apiCall: null, }); setIsModalVisible(false); }, []); // 开始验证流程 - const startVerification = useCallback(async (apiCall, options = {}) => { - const { preferredMethod, title, description } = options; + const startVerification = useCallback( + async (apiCall, options = {}) => { + const { preferredMethod, title, description } = options; - // 检查验证方式 - const methods = await checkVerificationMethods(); + // 检查验证方式 + const methods = await checkVerificationMethods(); - if (!methods.has2FA && !methods.hasPasskey) { - const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作'); - showError(errorMessage); - onError?.(new Error(errorMessage)); - return false; - } - - // 设置默认验证方式 - let defaultMethod = preferredMethod; - if (!defaultMethod) { - if (methods.hasPasskey && methods.passkeySupported) { - defaultMethod = 'passkey'; - } else if (methods.has2FA) { - defaultMethod = '2fa'; + if (!methods.has2FA && !methods.hasPasskey) { + const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作'); + showError(errorMessage); + onError?.(new Error(errorMessage)); + return false; } - } - setVerificationState(prev => ({ - ...prev, - method: defaultMethod, - apiCall, - title, - description - })); - setIsModalVisible(true); + // 设置默认验证方式 + let defaultMethod = preferredMethod; + if (!defaultMethod) { + if (methods.hasPasskey && methods.passkeySupported) { + defaultMethod = 'passkey'; + } else if (methods.has2FA) { + defaultMethod = '2fa'; + } + } - return true; - }, [checkVerificationMethods, onError, t]); + setVerificationState((prev) => ({ + ...prev, + method: defaultMethod, + apiCall, + title, + description, + })); + setIsModalVisible(true); + + return true; + }, + [checkVerificationMethods, onError, t], + ); // 执行验证 - const executeVerification = useCallback(async (method, code = '') => { - if (!verificationState.apiCall) { - showError(t('验证配置错误')); - return; - } - - setVerificationState(prev => ({ ...prev, loading: true })); - - try { - // 先调用验证 API,成功后后端会设置 session - await SecureVerificationService.verify(method, code); - - // 验证成功,调用业务 API(此时中间件会通过) - const result = await verificationState.apiCall(); - - // 显示成功消息 - if (successMessage) { - showSuccess(successMessage); + const executeVerification = useCallback( + async (method, code = '') => { + if (!verificationState.apiCall) { + showError(t('验证配置错误')); + return; } - // 调用成功回调 - onSuccess?.(result, method); + setVerificationState((prev) => ({ ...prev, loading: true })); - // 自动重置状态 - if (autoReset) { - resetState(); + try { + // 先调用验证 API,成功后后端会设置 session + await SecureVerificationService.verify(method, code); + + // 验证成功,调用业务 API(此时中间件会通过) + const result = await verificationState.apiCall(); + + // 显示成功消息 + if (successMessage) { + showSuccess(successMessage); + } + + // 调用成功回调 + onSuccess?.(result, method); + + // 自动重置状态 + if (autoReset) { + resetState(); + } + + return result; + } catch (error) { + showError(error.message || t('验证失败,请重试')); + onError?.(error); + throw error; + } finally { + setVerificationState((prev) => ({ ...prev, loading: false })); } - - return result; - } catch (error) { - showError(error.message || t('验证失败,请重试')); - onError?.(error); - throw error; - } finally { - setVerificationState(prev => ({ ...prev, loading: false })); - } - }, [verificationState.apiCall, successMessage, onSuccess, onError, autoReset, resetState, t]); + }, + [ + verificationState.apiCall, + successMessage, + onSuccess, + onError, + autoReset, + resetState, + t, + ], + ); // 设置验证码 const setVerificationCode = useCallback((code) => { - setVerificationState(prev => ({ ...prev, code })); + setVerificationState((prev) => ({ ...prev, code })); }, []); // 切换验证方式 const switchVerificationMethod = useCallback((method) => { - setVerificationState(prev => ({ ...prev, method, code: '' })); + setVerificationState((prev) => ({ ...prev, method, code: '' })); }, []); // 取消验证 @@ -171,20 +186,29 @@ export const useSecureVerification = ({ }, [resetState]); // 检查是否可以使用某种验证方式 - const canUseMethod = useCallback((method) => { - switch (method) { - case '2fa': - return verificationMethods.has2FA; - case 'passkey': - return verificationMethods.hasPasskey && verificationMethods.passkeySupported; - default: - return false; - } - }, [verificationMethods]); + const canUseMethod = useCallback( + (method) => { + switch (method) { + case '2fa': + return verificationMethods.has2FA; + case 'passkey': + return ( + verificationMethods.hasPasskey && + verificationMethods.passkeySupported + ); + default: + return false; + } + }, + [verificationMethods], + ); // 获取推荐的验证方式 const getRecommendedMethod = useCallback(() => { - if (verificationMethods.hasPasskey && verificationMethods.passkeySupported) { + if ( + verificationMethods.hasPasskey && + verificationMethods.passkeySupported + ) { return 'passkey'; } if (verificationMethods.has2FA) { @@ -200,22 +224,25 @@ export const useSecureVerification = ({ * @param {Object} options - 验证选项(同 startVerification) * @returns {Promise} */ - const withVerification = useCallback(async (apiCall, options = {}) => { - try { - // 直接尝试调用 API - return await apiCall(); - } catch (error) { - // 检查是否是需要验证的错误 - if (isVerificationRequiredError(error)) { - // 自动触发验证流程 - await startVerification(apiCall, options); - // 不抛出错误,让验证模态框处理 - return null; + const withVerification = useCallback( + async (apiCall, options = {}) => { + try { + // 直接尝试调用 API + return await apiCall(); + } catch (error) { + // 检查是否是需要验证的错误 + if (isVerificationRequiredError(error)) { + // 自动触发验证流程 + await startVerification(apiCall, options); + // 不抛出错误,让验证模态框处理 + return null; + } + // 其他错误继续抛出 + throw error; } - // 其他错误继续抛出 - throw error; - } - }, [startVerification]); + }, + [startVerification], + ); return { // 状态 @@ -238,9 +265,10 @@ export const useSecureVerification = ({ withVerification, // 新增:自动处理验证的包装函数 // 便捷属性 - hasAnyVerificationMethod: verificationMethods.has2FA || verificationMethods.hasPasskey, + hasAnyVerificationMethod: + verificationMethods.has2FA || verificationMethods.hasPasskey, isLoading: verificationState.loading, currentMethod: verificationState.method, - code: verificationState.code + code: verificationState.code, }; -}; \ No newline at end of file +}; diff --git a/web/src/hooks/users/useUsersData.jsx b/web/src/hooks/users/useUsersData.jsx index 38579c2fa..f906be543 100644 --- a/web/src/hooks/users/useUsersData.jsx +++ b/web/src/hooks/users/useUsersData.jsx @@ -86,7 +86,7 @@ export const useUsersData = () => { }; // Search users with keyword and group -const searchUsers = async ( + const searchUsers = async ( startIdx, pageSize, searchKeyword = null, diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 45f7181e4..8b4f1b411 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1285,7 +1285,6 @@ "可视化倍率设置": "Visual model ratio settings", "确定重置模型倍率吗?": "Confirm to reset model ratio?", "模型固定价格": "Model price per call", - "模型补全倍率(仅对自定义模型有效)": "Model completion ratio (only effective for custom models)", "保存模型倍率设置": "Save model ratio settings", "重置模型倍率": "Reset model ratio", "一次调用消耗多少刀,优先级大于模型倍率": "How much USD one call costs, priority over model ratio", @@ -2177,7 +2176,6 @@ "最后使用时间": "Last used time", "备份支持": "Backup support", "支持备份": "Supported", - "不支持": "Not supported", "备份状态": "Backup state", "已备份": "Backed up", "未备份": "Not backed up", @@ -2248,5 +2246,18 @@ "轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能": "Polling mode must be used with Redis and memory cache functions, otherwise the performance will be significantly reduced and the polling function will not be implemented", "common": { "changeLanguage": "Change Language" - } + }, + "充值账单": "Recharge Bills", + "订单号": "Order No.", + "支付金额": "Payment Amount", + "待支付": "Pending", + "加载失败": "Load failed", + "加载账单失败": "Failed to load bills", + "暂无充值记录": "No recharge records", + "账单": "Bills", + "补单": "Complete Order", + "补单成功": "Order completed successfully", + "补单失败": "Failed to complete order", + "确认补单": "Confirm Order Completion", + "是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?" } diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index ad60a6f59..e5db1125b 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -2238,5 +2238,18 @@ "配置 Passkey": "Configurer Passkey", "重置 2FA": "Réinitialiser 2FA", "重置 Passkey": "Réinitialiser le Passkey", - "默认使用系统名称": "Le nom du système est utilisé par défaut" + "默认使用系统名称": "Le nom du système est utilisé par défaut", + "充值账单": "Factures de recharge", + "订单号": "N° de commande", + "支付金额": "Montant payé", + "待支付": "En attente", + "加载失败": "Échec du chargement", + "加载账单失败": "Échec du chargement des factures", + "暂无充值记录": "Aucune recharge", + "账单": "Factures", + "补单": "Compléter la commande", + "补单成功": "Commande complétée avec succès", + "补单失败": "Échec de la complétion de la commande", + "确认补单": "Confirmer la complétion", + "是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?" } diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index fb6fbf990..dcb693ecd 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -94,5 +94,22 @@ "允许通过 Passkey 登录 & 认证": "允许通过 Passkey 登录 & 认证", "确认解绑 Passkey": "确认解绑 Passkey", "解绑后将无法使用 Passkey 登录,确定要继续吗?": "解绑后将无法使用 Passkey 登录,确定要继续吗?", - "确认解绑": "确认解绑" + "确认解绑": "确认解绑", + "充值账单": "充值账单", + "订单号": "订单号", + "支付金额": "支付金额", + "待支付": "待支付", + "加载失败": "加载失败", + "加载账单失败": "加载账单失败", + "暂无充值记录": "暂无充值记录", + "账单": "账单", + "支付方式": "支付方式", + "支付宝": "支付宝", + "微信": "微信", + "补单": "补单", + "补单成功": "补单成功", + "补单失败": "补单失败", + "确认补单": "确认补单", + "是否将该订单标记为成功并为用户入账?": "是否将该订单标记为成功并为用户入账?", + "操作": "操作" } diff --git a/web/src/pages/Setting/Chat/SettingsChats.jsx b/web/src/pages/Setting/Chat/SettingsChats.jsx index 01591c782..f7f309ac9 100644 --- a/web/src/pages/Setting/Chat/SettingsChats.jsx +++ b/web/src/pages/Setting/Chat/SettingsChats.jsx @@ -227,7 +227,7 @@ export default function SettingsChats(props) { const isDuplicate = chatConfigs.some( (config) => config.name === values.name && - (!isEdit || config.id !== editingConfig.id) + (!isEdit || config.id !== editingConfig.id), ); if (isDuplicate) { diff --git a/web/src/pages/Setting/Operation/SettingsLog.jsx b/web/src/pages/Setting/Operation/SettingsLog.jsx index a1f5a49fa..d309a12b0 100644 --- a/web/src/pages/Setting/Operation/SettingsLog.jsx +++ b/web/src/pages/Setting/Operation/SettingsLog.jsx @@ -18,7 +18,16 @@ For commercial licensing, please contact support@quantumnous.com */ import React, { useEffect, useState, useRef } from 'react'; -import { Button, Col, Form, Row, Spin, DatePicker, Typography, Modal } from '@douyinfe/semi-ui'; +import { + Button, + Col, + Form, + Row, + Spin, + DatePicker, + Typography, + Modal, +} from '@douyinfe/semi-ui'; import dayjs from 'dayjs'; import { useTranslation } from 'react-i18next'; import { @@ -90,40 +99,58 @@ export default function SettingsLog(props) { const targetTime = targetDate.format('YYYY-MM-DD HH:mm:ss'); const currentTime = now.format('YYYY-MM-DD HH:mm:ss'); const daysDiff = now.diff(targetDate, 'day'); - + Modal.confirm({ title: t('确认清除历史日志'), content: (

{t('当前时间')}: - {currentTime} + + {currentTime} +

{t('选择时间')}: - {targetTime} + + {targetTime} + {daysDiff > 0 && ( - ({t('约')} {daysDiff} {t('天前')}) + + {' '} + ({t('约')} {daysDiff} {t('天前')}) + )}

-
- ⚠️ {t('注意')}: +
+ + ⚠️ {t('注意')}: + {t('将删除')} - {targetTime} + + {targetTime} + {daysDiff > 0 && ( - ({t('约')} {daysDiff} {t('天前')}) + + {' '} + ({t('约')} {daysDiff} {t('天前')}) + )} {t('之前的所有日志')}

- {t('此操作不可恢复,请仔细确认时间后再操作!')} + + {t('此操作不可恢复,请仔细确认时间后再操作!')} +

), @@ -203,10 +230,18 @@ export default function SettingsLog(props) { }); }} /> - + {t('将清除选定时间之前的所有日志')} - diff --git a/web/src/services/secureVerification.js b/web/src/services/secureVerification.js index 93cdd0a4d..51f871a96 100644 --- a/web/src/services/secureVerification.js +++ b/web/src/services/secureVerification.js @@ -21,7 +21,7 @@ import { API, showError } from '../helpers'; import { prepareCredentialRequestOptions, buildAssertionResult, - isPasskeySupported + isPasskeySupported, } from '../helpers/passkey'; /** @@ -35,46 +35,54 @@ export class SecureVerificationService { */ static async checkAvailableVerificationMethods() { try { - const [twoFAResponse, passkeyResponse, passkeySupported] = await Promise.all([ - API.get('/api/user/2fa/status'), - API.get('/api/user/passkey'), - isPasskeySupported() - ]); + const [twoFAResponse, passkeyResponse, passkeySupported] = + await Promise.all([ + API.get('/api/user/2fa/status'), + API.get('/api/user/passkey'), + isPasskeySupported(), + ]); console.log('=== DEBUGGING VERIFICATION METHODS ==='); console.log('2FA Response:', JSON.stringify(twoFAResponse, null, 2)); - console.log('Passkey Response:', JSON.stringify(passkeyResponse, null, 2)); - - const has2FA = twoFAResponse.data?.success && twoFAResponse.data?.data?.enabled === true; - const hasPasskey = passkeyResponse.data?.success && passkeyResponse.data?.data?.enabled === true; - + console.log( + 'Passkey Response:', + JSON.stringify(passkeyResponse, null, 2), + ); + + const has2FA = + twoFAResponse.data?.success && + twoFAResponse.data?.data?.enabled === true; + const hasPasskey = + passkeyResponse.data?.success && + passkeyResponse.data?.data?.enabled === true; + console.log('has2FA calculation:', { success: twoFAResponse.data?.success, dataExists: !!twoFAResponse.data?.data, enabled: twoFAResponse.data?.data?.enabled, - result: has2FA + result: has2FA, }); - + console.log('hasPasskey calculation:', { success: passkeyResponse.data?.success, dataExists: !!passkeyResponse.data?.data, enabled: passkeyResponse.data?.data?.enabled, - result: hasPasskey + result: hasPasskey, }); const result = { has2FA, hasPasskey, - passkeySupported + passkeySupported, }; - + return result; } catch (error) { console.error('Failed to check verification methods:', error); return { has2FA: false, hasPasskey: false, - passkeySupported: false + passkeySupported: false, }; } } @@ -92,7 +100,7 @@ export class SecureVerificationService { // 调用通用验证 API,验证成功后后端会设置 session const verifyResponse = await API.post('/api/verify', { method: '2fa', - code: code.trim() + code: code.trim(), }); if (!verifyResponse.data?.success) { @@ -115,7 +123,9 @@ export class SecureVerificationService { } // 准备WebAuthn选项 - const publicKey = prepareCredentialRequestOptions(beginResponse.data.data.options); + const publicKey = prepareCredentialRequestOptions( + beginResponse.data.data.options, + ); // 执行WebAuthn验证 const credential = await navigator.credentials.get({ publicKey }); @@ -127,14 +137,17 @@ export class SecureVerificationService { const assertionResult = buildAssertionResult(credential); // 完成验证 - const finishResponse = await API.post('/api/user/passkey/verify/finish', assertionResult); + const finishResponse = await API.post( + '/api/user/passkey/verify/finish', + assertionResult, + ); if (!finishResponse.data?.success) { throw new Error(finishResponse.data?.message || '验证失败'); } // 调用通用验证 API 设置 session(Passkey 验证已完成) const verifyResponse = await API.post('/api/verify', { - method: 'passkey' + method: 'passkey', }); if (!verifyResponse.data?.success) { @@ -191,27 +204,29 @@ export const createApiCalls = { * @param {string} method - HTTP方法,默认为 'POST' * @param {Object} extraData - 额外的请求数据 */ - custom: (url, method = 'POST', extraData = {}) => async () => { - // 新系统中,验证已通过中间件处理 - const data = extraData; + custom: + (url, method = 'POST', extraData = {}) => + async () => { + // 新系统中,验证已通过中间件处理 + const data = extraData; - let response; - switch (method.toUpperCase()) { - case 'GET': - response = await API.get(url, { params: data }); - break; - case 'POST': - response = await API.post(url, data); - break; - case 'PUT': - response = await API.put(url, data); - break; - case 'DELETE': - response = await API.delete(url, { data }); - break; - default: - throw new Error(`不支持的HTTP方法: ${method}`); - } - return response.data; - } -}; \ No newline at end of file + let response; + switch (method.toUpperCase()) { + case 'GET': + response = await API.get(url, { params: data }); + break; + case 'POST': + response = await API.post(url, data); + break; + case 'PUT': + response = await API.put(url, data); + break; + case 'DELETE': + response = await API.delete(url, { data }); + break; + default: + throw new Error(`不支持的HTTP方法: ${method}`); + } + return response.data; + }, +};