diff --git a/common/constants.go b/common/constants.go index d6b4fb52284c..c214fab5a215 100644 --- a/common/constants.go +++ b/common/constants.go @@ -124,6 +124,7 @@ var TelegramBotName = "" var QuotaForNewUser = 0 var QuotaForInviter = 0 var QuotaForInvitee = 0 +var AffiliateRewardsEnabled = true var ChannelDisableThreshold = 5.0 var AutomaticDisableChannelEnabled = false var AutomaticEnableChannelEnabled = false diff --git a/controller/misc.go b/controller/misc.go index 7343b12f10a3..495d8ca9df37 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -92,6 +92,7 @@ func GetStatus(c *gin.Context) { "register_enabled": common.RegisterEnabled, "password_login_enabled": common.PasswordLoginEnabled, "password_register_enabled": common.PasswordRegisterEnabled, + "affiliate_rewards_enabled": common.AffiliateRewardsEnabled, "default_use_auto_group": setting.DefaultUseAutoGroup, "usd_exchange_rate": operation_setting.USDExchangeRate, diff --git a/controller/model.go b/controller/model.go index 1d759301bc7e..5ab70decec10 100644 --- a/controller/model.go +++ b/controller/model.go @@ -265,11 +265,15 @@ func ListModels(c *gin.Context, modelType int) { switch modelType { case constant.ChannelTypeAnthropic: useranthropicModels := make([]dto.AnthropicModel, len(userOpenAiModels)) - for i, model := range userOpenAiModels { + for i, m := range userOpenAiModels { + id := m.Id + if !strings.HasPrefix(id, "claude-") { + id = "claude-" + id + } useranthropicModels[i] = dto.AnthropicModel{ - ID: model.Id, - CreatedAt: time.Unix(int64(model.Created), 0).UTC().Format(time.RFC3339), - DisplayName: model.Id, + ID: id, + CreatedAt: time.Unix(int64(m.Created), 0).UTC().Format(time.RFC3339), + DisplayName: m.Id, Type: "model", } } @@ -329,11 +333,19 @@ func EnabledListModels(c *gin.Context) { func RetrieveModel(c *gin.Context, modelType int) { modelId := c.Param("model") - if aiModel, ok := openAIModelsMap[modelId]; ok { + lookupId := modelId + if modelType == constant.ChannelTypeAnthropic { + lookupId = resolveAnthropicModelID(modelId) + } + if aiModel, ok := openAIModelsMap[lookupId]; ok { switch modelType { case constant.ChannelTypeAnthropic: + id := aiModel.Id + if !strings.HasPrefix(id, "claude-") { + id = "claude-" + id + } c.JSON(200, dto.AnthropicModel{ - ID: aiModel.Id, + ID: id, CreatedAt: time.Unix(int64(aiModel.Created), 0).UTC().Format(time.RFC3339), DisplayName: aiModel.Id, Type: "model", @@ -353,3 +365,17 @@ func RetrieveModel(c *gin.Context, modelType int) { }) } } + +func resolveAnthropicModelID(id string) string { + if !strings.HasPrefix(id, "claude-") { + return id + } + if _, ok := openAIModelsMap[id]; ok { + return id + } + stripped := id[len("claude-"):] + if stripped == "" { + return id + } + return stripped +} diff --git a/controller/status_affiliate_rewards_test.go b/controller/status_affiliate_rewards_test.go new file mode 100644 index 000000000000..6ec9cf1f0ea6 --- /dev/null +++ b/controller/status_affiliate_rewards_test.go @@ -0,0 +1,74 @@ +package controller + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetStatusIncludesAffiliateRewardsEnabled(t *testing.T) { + originalEnabled := common.AffiliateRewardsEnabled + originalOptionMap := common.OptionMap + t.Cleanup(func() { + common.AffiliateRewardsEnabled = originalEnabled + common.OptionMap = originalOptionMap + }) + + common.AffiliateRewardsEnabled = false + common.OptionMap = map[string]string{ + "HeaderNavModules": "[]", + "SidebarModulesAdmin": "[]", + } + + gin.SetMode(gin.TestMode) + recorder := httptest.NewRecorder() + context, _ := gin.CreateTestContext(recorder) + context.Request = httptest.NewRequest(http.MethodGet, "/api/status", nil) + + GetStatus(context) + + require.Equal(t, http.StatusOK, recorder.Code) + + var payload struct { + Success bool `json:"success"` + Data struct { + AffiliateRewardsEnabled bool `json:"affiliate_rewards_enabled"` + } `json:"data"` + } + require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &payload)) + require.True(t, payload.Success) + assert.False(t, payload.Data.AffiliateRewardsEnabled) +} + +func TestGetTopUpInfoIncludesAffiliateRewardsEnabled(t *testing.T) { + originalEnabled := common.AffiliateRewardsEnabled + t.Cleanup(func() { + common.AffiliateRewardsEnabled = originalEnabled + }) + + common.AffiliateRewardsEnabled = false + + gin.SetMode(gin.TestMode) + recorder := httptest.NewRecorder() + context, _ := gin.CreateTestContext(recorder) + context.Request = httptest.NewRequest(http.MethodGet, "/api/user/topup/info", nil) + + GetTopUpInfo(context) + + require.Equal(t, http.StatusOK, recorder.Code) + + var payload struct { + Success bool `json:"success"` + Data struct { + AffiliateRewardsEnabled bool `json:"affiliate_rewards_enabled"` + } `json:"data"` + } + require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &payload)) + require.True(t, payload.Success) + assert.False(t, payload.Data.AffiliateRewardsEnabled) +} diff --git a/controller/topup.go b/controller/topup.go index 390f53f7dce8..c25e818d455f 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -102,6 +102,7 @@ func GetTopUpInfo(c *gin.Context) { "enable_waffo_topup": enableWaffo, "enable_waffo_pancake_topup": enableWaffoPancake, "enable_redemption": complianceConfirmed, + "affiliate_rewards_enabled": common.AffiliateRewardsEnabled, "payment_compliance_confirmed": complianceConfirmed, "payment_compliance_terms_version": operation_setting.CurrentComplianceTermsVersion, "waffo_pay_methods": func() interface{} { diff --git a/middleware/distributor.go b/middleware/distributor.go index 7decf0e28728..d8cd3eb8c05f 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -39,6 +39,9 @@ func Distribute() func(c *gin.Context) { abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidRequest, map[string]any{"Error": err.Error()})) return } + if shouldSelectChannel && strings.HasPrefix(c.Request.URL.Path, "/v1/messages") { + modelRequest.Model = resolveAnthropicModelForRouting(modelRequest.Model) + } if ok { id, err := strconv.Atoi(channelId.(string)) if err != nil { @@ -170,6 +173,24 @@ func Distribute() func(c *gin.Context) { } } +// resolveAnthropicModelForRouting strips the `claude-` prefix from model names +// that were disguised for Claude Code Desktop compatibility. Real Claude models +// (e.g. claude-sonnet-4-6) are returned unchanged because they exist in the +// ability system; disguised models (e.g. claude-gpt-4o) have their prefix stripped. +func resolveAnthropicModelForRouting(modelName string) string { + if !strings.HasPrefix(modelName, "claude-") { + return modelName + } + if model.IsModelInAnyAbility(modelName) { + return modelName + } + stripped := modelName[len("claude-"):] + if stripped == "" { + return modelName + } + return stripped +} + // channelSupportsRequestPath reports whether a channel can serve the request path. // Only Advanced Custom (type 58) channels are path-checked; all other channel types // always pass. A type-58 channel is usable only when one of its routes matches. diff --git a/model/ability.go b/model/ability.go index d950a6adbfc4..828745758190 100644 --- a/model/ability.go +++ b/model/ability.go @@ -54,6 +54,22 @@ func GetEnabledModels() []string { return models } +func IsModelInAnyAbility(modelName string) bool { + if !common.MemoryCacheEnabled { + var count int64 + DB.Model(&Ability{}).Where("model = ? and enabled = ?", modelName, true).Limit(1).Count(&count) + return count > 0 + } + channelSyncLock.RLock() + defer channelSyncLock.RUnlock() + for _, modelMap := range group2model2channels { + if channels, ok := modelMap[modelName]; ok && len(channels) > 0 { + return true + } + } + return false +} + func GetAllEnableAbilities() []Ability { var abilities []Ability DB.Find(&abilities, "enabled = ?", true) diff --git a/model/option.go b/model/option.go index e7fda5231be7..d14e0574a42d 100644 --- a/model/option.go +++ b/model/option.go @@ -134,6 +134,7 @@ func InitOptionMap() { common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser) common.OptionMap["QuotaForInviter"] = strconv.Itoa(common.QuotaForInviter) common.OptionMap["QuotaForInvitee"] = strconv.Itoa(common.QuotaForInvitee) + common.OptionMap["AffiliateRewardsEnabled"] = strconv.FormatBool(common.AffiliateRewardsEnabled) common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold) common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota) common.OptionMap["ModelRequestRateLimitCount"] = strconv.Itoa(setting.ModelRequestRateLimitCount) @@ -351,6 +352,8 @@ func updateOptionMap(key string, value string) (err error) { common.TaskEnabled = boolValue case "DataExportEnabled": common.DataExportEnabled = boolValue + case "AffiliateRewardsEnabled": + common.AffiliateRewardsEnabled = boolValue case "DefaultCollapseSidebar": common.DefaultCollapseSidebar = boolValue case "MjNotifyEnabled": diff --git a/model/option_affiliate_rewards_test.go b/model/option_affiliate_rewards_test.go new file mode 100644 index 000000000000..1fa6345e024b --- /dev/null +++ b/model/option_affiliate_rewards_test.go @@ -0,0 +1,26 @@ +package model + +import ( + "testing" + + "github.com/QuantumNous/new-api/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdateOptionMapUpdatesAffiliateRewardsEnabled(t *testing.T) { + originalEnabled := common.AffiliateRewardsEnabled + originalOptionMap := common.OptionMap + t.Cleanup(func() { + common.AffiliateRewardsEnabled = originalEnabled + common.OptionMap = originalOptionMap + }) + + common.OptionMap = map[string]string{} + common.AffiliateRewardsEnabled = true + + require.NoError(t, updateOptionMap("AffiliateRewardsEnabled", "false")) + + assert.False(t, common.AffiliateRewardsEnabled) + assert.Equal(t, "false", common.OptionMap["AffiliateRewardsEnabled"]) +} diff --git a/web/src/features/auth/types.ts b/web/src/features/auth/types.ts index afaa4b716042..469467e06fac 100644 --- a/web/src/features/auth/types.ts +++ b/web/src/features/auth/types.ts @@ -128,6 +128,7 @@ export interface SystemStatus { custom_currency_symbol?: string custom_currency_exchange_rate?: number demo_site_enabled?: boolean + affiliate_rewards_enabled?: boolean user_agreement_enabled?: boolean privacy_policy_enabled?: boolean oauth_register_enabled?: boolean @@ -173,6 +174,7 @@ export interface SystemStatus { custom_currency_symbol?: string custom_currency_exchange_rate?: number demo_site_enabled?: boolean + affiliate_rewards_enabled?: boolean user_agreement_enabled?: boolean privacy_policy_enabled?: boolean oauth_register_enabled?: boolean diff --git a/web/src/features/system-settings/billing/index.tsx b/web/src/features/system-settings/billing/index.tsx index a49bd0a85d85..e4e6626311d4 100644 --- a/web/src/features/system-settings/billing/index.tsx +++ b/web/src/features/system-settings/billing/index.tsx @@ -29,6 +29,7 @@ const defaultBillingSettings: BillingSettings = { PreConsumedQuota: 0, QuotaForInviter: 0, QuotaForInvitee: 0, + AffiliateRewardsEnabled: true, TopUpLink: '', 'general_setting.docs_link': '', 'quota_setting.enable_free_model_pre_consume': true, diff --git a/web/src/features/system-settings/billing/section-registry.tsx b/web/src/features/system-settings/billing/section-registry.tsx index 43cd0e986b91..60a333d221c6 100644 --- a/web/src/features/system-settings/billing/section-registry.tsx +++ b/web/src/features/system-settings/billing/section-registry.tsx @@ -63,6 +63,7 @@ const BILLING_SECTIONS = [ PreConsumedQuota: settings.PreConsumedQuota, QuotaForInviter: settings.QuotaForInviter, QuotaForInvitee: settings.QuotaForInvitee, + AffiliateRewardsEnabled: settings.AffiliateRewardsEnabled, TopUpLink: settings.TopUpLink, general_setting: { docs_link: settings['general_setting.docs_link'], diff --git a/web/src/features/system-settings/general/quota-settings-section.tsx b/web/src/features/system-settings/general/quota-settings-section.tsx index b72838ace6ed..4adde5aa312f 100644 --- a/web/src/features/system-settings/general/quota-settings-section.tsx +++ b/web/src/features/system-settings/general/quota-settings-section.tsx @@ -55,6 +55,7 @@ const quotaSchema = z.object({ PreConsumedQuota: z.coerce.number().min(0), QuotaForInviter: z.coerce.number().min(0), QuotaForInvitee: z.coerce.number().min(0), + AffiliateRewardsEnabled: z.boolean(), TopUpLink: z.string(), general_setting: z.object({ docs_link: z.string(), @@ -237,6 +238,32 @@ export function QuotaSettingsSection({ )} /> + + ( + + + {t('Enable Invitation Rewards')} + + {t( + 'When disabled, users will not see the invitation rewards panel.' + )} + + + + + + + )} + /> + + ('') const [affiliateLink, setAffiliateLink] = useState('') - const [loading, setLoading] = useState(true) + const [loading, setLoading] = useState(enabled) const [transferring, setTransferring] = useState(false) const { copyToClipboard } = useCopyToClipboard() // Fetch affiliate code const fetchAffiliateCode = useCallback(async () => { + if (!enabled) { + setAffiliateCode('') + setAffiliateLink('') + setLoading(false) + return + } + try { setLoading(true) const response = await getAffiliateCode() @@ -54,7 +66,7 @@ export function useAffiliate() { } finally { setLoading(false) } - }, []) + }, [enabled]) // Copy affiliate link const copyAffiliateLink = useCallback(() => { @@ -84,8 +96,15 @@ export function useAffiliate() { }, []) useEffect(() => { - fetchAffiliateCode() - }, [fetchAffiliateCode]) + if (enabled) { + fetchAffiliateCode() + return + } + + setAffiliateCode('') + setAffiliateLink('') + setLoading(false) + }, [enabled, fetchAffiliateCode]) return { affiliateCode, diff --git a/web/src/features/wallet/index.tsx b/web/src/features/wallet/index.tsx index 5a9cb9e08702..ff3abef86f39 100644 --- a/web/src/features/wallet/index.tsx +++ b/web/src/features/wallet/index.tsx @@ -46,6 +46,7 @@ import { getDefaultPaymentType, getMinTopupAmount, dispatchSelectedPayment, + shouldShowAffiliateRewards, } from './lib' import type { UserWalletData, @@ -83,6 +84,9 @@ export function Wallet(props: WalletProps) { const { status } = useStatus() const { currency } = useSystemConfig() const { topupInfo, presetAmounts, loading: topupLoading } = useTopupInfo() + const affiliateRewardsEnabled = shouldShowAffiliateRewards( + topupInfo?.affiliate_rewards_enabled + ) // Calculate effective exchange rate - when display type is USD, use rate of 1 const effectiveUsdExchangeRate = useMemo(() => { @@ -102,7 +106,7 @@ export function Wallet(props: WalletProps) { loading: affiliateLoading, transferQuota, transferring, - } = useAffiliate() + } = useAffiliate({ enabled: affiliateRewardsEnabled }) const { redeeming, redeemCode } = useRedemption() const { processing: creemProcessing, processCreemPayment } = useCreemPayment() const { processing: waffoProcessing, processWaffoPayment } = useWaffoPayment() @@ -339,15 +343,17 @@ export function Wallet(props: WalletProps) { /> - setTransferDialogOpen(true)} - complianceConfirmed={ - topupInfo?.payment_compliance_confirmed !== false - } - loading={affiliateLoading} - /> + {affiliateRewardsEnabled ? ( + setTransferDialogOpen(true)} + complianceConfirmed={ + topupInfo?.payment_compliance_confirmed !== false + } + loading={affiliateLoading} + /> + ) : null} diff --git a/web/src/features/wallet/lib/affiliate.test.ts b/web/src/features/wallet/lib/affiliate.test.ts new file mode 100644 index 000000000000..6e86b31d466a --- /dev/null +++ b/web/src/features/wallet/lib/affiliate.test.ts @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict' +import { describe, test } from 'node:test' + +import { shouldShowAffiliateRewards } from './affiliate.ts' + +describe('wallet affiliate rewards visibility', () => { + test('shows rewards only when backend explicitly enables them', () => { + assert.equal(shouldShowAffiliateRewards(true), true) + assert.equal(shouldShowAffiliateRewards(false), false) + assert.equal(shouldShowAffiliateRewards(undefined), false) + }) +}) diff --git a/web/src/features/wallet/lib/affiliate.ts b/web/src/features/wallet/lib/affiliate.ts index 3e4a12f8ba31..15028bc99b46 100644 --- a/web/src/features/wallet/lib/affiliate.ts +++ b/web/src/features/wallet/lib/affiliate.ts @@ -27,3 +27,12 @@ export function generateAffiliateLink(affCode: string): string { if (typeof window === 'undefined') return '' return `${window.location.origin}/sign-up?aff=${affCode}` } + +/** + * Affiliate rewards are visible only when the backend explicitly enables them. + * Missing data is treated as disabled to avoid showing the block before the + * system setting is available. + */ +export function shouldShowAffiliateRewards(enabled?: boolean): boolean { + return enabled === true +} diff --git a/web/src/features/wallet/types.ts b/web/src/features/wallet/types.ts index 5da08f09f140..7a865d0538c0 100644 --- a/web/src/features/wallet/types.ts +++ b/web/src/features/wallet/types.ts @@ -156,6 +156,8 @@ export interface TopupInfo { payment_compliance_confirmed?: boolean /** Current compliance terms version */ payment_compliance_terms_version?: string + /** Whether affiliate rewards are enabled */ + affiliate_rewards_enabled?: boolean } /** diff --git a/web/src/hooks/use-system-config.ts b/web/src/hooks/use-system-config.ts index 23f92a633137..6f2195c299a7 100644 --- a/web/src/hooks/use-system-config.ts +++ b/web/src/hooks/use-system-config.ts @@ -47,6 +47,7 @@ interface StatusApiResponse { usd_exchange_rate?: number custom_currency_symbol?: string custom_currency_exchange_rate?: number + affiliate_rewards_enabled?: boolean } } @@ -98,6 +99,7 @@ export function mapStatusDataToConfig( footerHtml: data.footer_html, demoSiteEnabled: data.demo_site_enabled, displayTokenStatEnabled: data.display_token_stat_enabled, + affiliateRewardsEnabled: data.affiliate_rewards_enabled === true, currency, } } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index b48096a7fcfa..ff824d1fff34 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1583,6 +1583,7 @@ "Enable GitHub OAuth": "Enable GitHub OAuth", "Enable Groups": "Enable Groups", "Enable if this is an OpenRouter enterprise account with special response format": "Enable if this is an OpenRouter enterprise account with special response format", + "Enable Invitation Rewards": "Enable Invitation Rewards", "Enable io.net deployments": "Enable io.net deployments", "Enable io.net model deployment service in console": "Enable io.net model deployment service in console", "Enable LinuxDO OAuth": "Enable LinuxDO OAuth", @@ -5171,6 +5172,7 @@ "When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.", "When billed as {{group}}": "When billed as {{group}}", "When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.", + "When disabled, users will not see the invitation rewards panel.": "When disabled, users will not see the invitation rewards panel.", "When enabled, if channels in the current group fail, it will try channels in the next group in order.": "When enabled, if channels in the current group fail, it will try channels in the next group in order.", "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.", "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 638cf9abadd1..0ef3e31beb6e 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -1583,6 +1583,7 @@ "Enable GitHub OAuth": "Activer GitHub OAuth", "Enable Groups": "Activer les groupes", "Enable if this is an OpenRouter enterprise account with special response format": "Activer si c'est un compte d'entreprise OpenRouter avec un format de réponse spécial", + "Enable Invitation Rewards": "Activer les récompenses de parrainage", "Enable io.net deployments": "Activer les déploiements io.net", "Enable io.net model deployment service in console": "Activer le service de déploiement de modèles io.net dans la console", "Enable LinuxDO OAuth": "Activer LinuxDO OAuth", @@ -5171,6 +5172,7 @@ "When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "Quand un jeton utilise le groupe auto, le système essaie les groupes de haut en bas jusqu’à trouver un groupe disponible.", "When billed as {{group}}": "Facturé sous {{group}}", "When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "Si les conditions sont remplies, le prix final est multiplié par X. Plusieurs correspondances se multiplient ; les valeurs < 1 agissent comme des remises.", + "When disabled, users will not see the invitation rewards panel.": "Lorsque cette option est désactivée, les utilisateurs ne voient pas le panneau des récompenses de parrainage.", "When enabled, if channels in the current group fail, it will try channels in the next group in order.": "Lorsqu'elle est activée, si les canaux du groupe actuel échouent, le système essaiera les canaux du groupe suivant dans l'ordre.", "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Lorsque cette option est activée, conserver l'entrée d'affinité même si le canal affinitaire est désactivé ou n'est plus utilisable pour le groupe/modèle actuel. Laissez-la désactivée pour supprimer l'entrée et sélectionner un autre canal.", "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "Lorsqu'activé, les corps de requête volumineux sont temporairement stockés sur disque, réduisant considérablement l'utilisation mémoire. SSD recommandé.", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index 3b9d4ce693cc..1e2aa18065ec 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -1583,6 +1583,7 @@ "Enable GitHub OAuth": "GitHub OAuthを有効にする", "Enable Groups": "グループを有効にする", "Enable if this is an OpenRouter enterprise account with special response format": "特別な応答形式を持つOpenRouterエンタープライズアカウントである場合に有効にします", + "Enable Invitation Rewards": "招待報酬を有効化", "Enable io.net deployments": "io.net デプロイを有効化", "Enable io.net model deployment service in console": "コンソールで io.net モデルデプロイサービスを有効化", "Enable LinuxDO OAuth": "LinuxDO OAuthを有効にする", @@ -5171,6 +5172,7 @@ "When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "トークンが auto グループを使用すると、システムは上から順に利用可能なグループを探します。", "When billed as {{group}}": "{{group}} として課金時", "When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "条件に一致したとき、最終価格に X を掛けます。複数一致は掛け合わさり、1 未満は割引として効きます。", + "When disabled, users will not see the invitation rewards panel.": "無効にすると、ユーザーには招待報酬パネルが表示されません。", "When enabled, if channels in the current group fail, it will try channels in the next group in order.": "有効にすると、現在のグループのチャネルが失敗した場合、次のグループのチャネルを順番に試します。", "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "有効にすると、アフィニティチャネルが無効化された、または現在のグループ/モデルで利用できなくなった場合でも、そのアフィニティエントリを保持します。無効のままにすると、エントリを削除して別のチャネルを選択します。", "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "有効にすると、大きなリクエストボディはメモリではなくディスクに一時保存され、メモリ使用量が大幅に削減されます。SSD環境での使用を推奨します。", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 853dc874d463..2dd4885f187d 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -1583,6 +1583,7 @@ "Enable GitHub OAuth": "Включить GitHub OAuth", "Enable Groups": "Включить группы", "Enable if this is an OpenRouter enterprise account with special response format": "Включите, если это корпоративный аккаунт OpenRouter со специальным форматом ответа", + "Enable Invitation Rewards": "Включить вознаграждения за приглашения", "Enable io.net deployments": "Включить развертывания io.net", "Enable io.net model deployment service in console": "Включить сервис развертывания моделей io.net в консоли", "Enable LinuxDO OAuth": "Включить LinuxDO OAuth", @@ -5171,6 +5172,7 @@ "When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "Когда токен использует группу auto, система перебирает группы сверху вниз, пока не найдёт доступную.", "When billed as {{group}}": "При тарификации по {{group}}", "When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "При совпадении условий итоговая цена умножается на X. Несколько совпадений умножаются вместе; значения < 1 действуют как скидки.", + "When disabled, users will not see the invitation rewards panel.": "Если отключено, пользователи не увидят панель вознаграждений за приглашения.", "When enabled, if channels in the current group fail, it will try channels in the next group in order.": "Если включено, при сбое каналов в текущей группе система попробует каналы следующей группы по порядку.", "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Если включено, запись привязки сохраняется, даже когда привязанный канал отключён или больше не подходит для текущей группы/модели. Оставьте выключенным, чтобы удалять запись и выбирать другой канал.", "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "При включении большие тела запросов временно сохраняются на диске, что значительно снижает использование памяти. Рекомендуется SSD.", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index cdc1a12626e1..ef7543543ed9 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -1583,6 +1583,7 @@ "Enable GitHub OAuth": "Kích hoạt GitHub OAuth", "Enable Groups": "Bật Nhóm", "Enable if this is an OpenRouter enterprise account with special response format": "Bật nếu đây là tài khoản doanh nghiệp OpenRouter với định dạng phản hồi đặc biệt", + "Enable Invitation Rewards": "Bật phần thưởng mời bạn", "Enable io.net deployments": "Bật triển khai io.net", "Enable io.net model deployment service in console": "Bật dịch vụ triển khai mô hình io.net trong bảng điều khiển", "Enable LinuxDO OAuth": "Bật LinuxDO OAuth", @@ -5171,6 +5172,7 @@ "When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "Khi token dùng nhóm auto, hệ thống thử các nhóm từ trên xuống dưới cho đến khi tìm được nhóm khả dụng.", "When billed as {{group}}": "Khi tính phí theo {{group}}", "When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "Khi thỏa điều kiện, giá cuối nhân với X. Nhiều điều kiện khớp nhân lại với nhau; giá trị < 1 hoạt động như giảm giá.", + "When disabled, users will not see the invitation rewards panel.": "Khi tắt, người dùng sẽ không thấy bảng phần thưởng mời bạn.", "When enabled, if channels in the current group fail, it will try channels in the next group in order.": "Khi được bật, nếu các kênh trong nhóm hiện tại thất bại, hệ thống sẽ thử các kênh của nhóm tiếp theo theo thứ tự.", "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "Khi bật, giữ mục ưu tiên ngay cả khi kênh ưu tiên bị tắt hoặc không còn dùng được cho nhóm/mô hình hiện tại. Để tắt để xóa mục đó và chọn kênh khác.", "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "Khi bật, nội dung yêu cầu lớn sẽ được lưu tạm trên đĩa thay vì bộ nhớ, giảm đáng kể việc sử dụng bộ nhớ. Khuyến nghị dùng SSD.", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index f8b5fafd8d2c..0f3ef2fc4dc3 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -1583,6 +1583,7 @@ "Enable GitHub OAuth": "啟用 GitHub OAuth", "Enable Groups": "啟用分組", "Enable if this is an OpenRouter enterprise account with special response format": "如果這是具有特殊回應格式的 OpenRouter 企業用戶,則啟用", + "Enable Invitation Rewards": "啟用邀請返利", "Enable io.net deployments": "啟用 io.net 部署", "Enable io.net model deployment service in console": "在控制台啟用 io.net 模型部署服務", "Enable LinuxDO OAuth": "啟用 LinuxDO OAuth", @@ -5171,6 +5172,7 @@ "When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "當令牌使用 auto 分組時,系統會按從上到下的順序嘗試,直到找到可用分組。", "When billed as {{group}}": "按 {{group}} 收費時", "When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "條件滿足時,最終價格乘以 X;多條命中的倍率會相乘;小於 1 的值為折扣。", + "When disabled, users will not see the invitation rewards panel.": "關閉後,使用者將不會看到邀請返利區塊。", "When enabled, if channels in the current group fail, it will try channels in the next group in order.": "開啟後,目前分組渠道失敗時會按順序嘗試下一個分組的渠道。", "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "開啟後,親和到的渠道被停用,或不再適用於目前分組/模型時,仍保留這條親和;關閉時會刪除並重新選擇渠道。", "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "啟用磁碟緩存後,大請求體將臨時儲存到磁碟而非記憶體,可顯著降低記憶體佔用。建議在 SSD 環境下使用。", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 842991e714f0..9e041a587f9d 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -1583,6 +1583,7 @@ "Enable GitHub OAuth": "启用 GitHub OAuth", "Enable Groups": "启用分组", "Enable if this is an OpenRouter enterprise account with special response format": "如果这是具有特殊响应格式的 OpenRouter 企业账户,则启用", + "Enable Invitation Rewards": "启用邀请返利", "Enable io.net deployments": "启用 io.net 部署", "Enable io.net model deployment service in console": "在控制台启用 io.net 模型部署服务", "Enable LinuxDO OAuth": "启用 LinuxDO OAuth", @@ -5171,6 +5172,7 @@ "When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.": "当令牌使用 auto 分组时,系统会按从上到下的顺序尝试,直到找到可用分组。", "When billed as {{group}}": "按 {{group}} 计费时", "When conditions match, the final price is multiplied by X. Multiple matches multiply together; values < 1 act as discounts.": "条件满足时,最终价格乘以 X;多条命中的倍率会相乘;小于 1 的值为折扣。", + "When disabled, users will not see the invitation rewards panel.": "关闭后,用户将不会看到邀请返利板块。", "When enabled, if channels in the current group fail, it will try channels in the next group in order.": "开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道。", "When enabled, keep the affinity entry even if the affinity channel is disabled or no longer usable for the current group/model. Leave it off to delete the entry and select another channel.": "开启后,亲和到的渠道被禁用,或不再适用于当前分组/模型时,仍保留这条亲和;关闭时会删除并重新选择渠道。", "When enabled, large request bodies are temporarily stored on disk instead of memory, significantly reducing memory usage. SSD recommended.": "启用磁盘缓存后,大请求体将临时存储到磁盘而非内存,可显著降低内存占用。建议在 SSD 环境下使用。", diff --git a/web/src/stores/system-config-store.ts b/web/src/stores/system-config-store.ts index 570fc45af4e8..5766eff0e948 100644 --- a/web/src/stores/system-config-store.ts +++ b/web/src/stores/system-config-store.ts @@ -44,6 +44,7 @@ export interface SystemConfig { footerHtml?: string demoSiteEnabled?: boolean displayTokenStatEnabled?: boolean + affiliateRewardsEnabled?: boolean currency: CurrencyConfig }