diff --git a/web/default/src/features/system-settings/billing/index.tsx b/web/default/src/features/system-settings/billing/index.tsx index daad5066..2b2c0949 100644 --- a/web/default/src/features/system-settings/billing/index.tsx +++ b/web/default/src/features/system-settings/billing/index.tsx @@ -55,6 +55,7 @@ const defaultBillingSettings: BillingSettings = { GroupRatio: '', UserUsableGroups: '', GroupGroupRatio: '', + DomainRatio: '', AutoGroups: '', DefaultUseAutoGroup: false, 'group_ratio_setting.group_special_usable_group': '{}', diff --git a/web/default/src/features/system-settings/billing/section-registry.tsx b/web/default/src/features/system-settings/billing/section-registry.tsx index 1a1dc8a2..3342b5c6 100644 --- a/web/default/src/features/system-settings/billing/section-registry.tsx +++ b/web/default/src/features/system-settings/billing/section-registry.tsx @@ -44,6 +44,7 @@ const getGroupDefaults = (settings: BillingSettings) => ({ GroupRatio: settings.GroupRatio, UserUsableGroups: settings.UserUsableGroups, GroupGroupRatio: settings.GroupGroupRatio, + DomainRatio: settings.DomainRatio, AutoGroups: settings.AutoGroups, DefaultUseAutoGroup: settings.DefaultUseAutoGroup, GroupSpecialUsableGroup: diff --git a/web/default/src/features/system-settings/models/domain-ratio-editor.tsx b/web/default/src/features/system-settings/models/domain-ratio-editor.tsx new file mode 100644 index 00000000..9af9fa74 --- /dev/null +++ b/web/default/src/features/system-settings/models/domain-ratio-editor.tsx @@ -0,0 +1,191 @@ +/* +Copyright (C) 2023-2026 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 { useEffect, useRef, useState } from 'react' +import { Plus, Trash2 } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { Input } from '@/components/ui/input' + +type Row = { + id: number + domain: string + group: string + ratio: string +} + +type DomainRatioEditorProps = { + value: string + onChange: (value: string) => void +} + +let rowSeq = 0 +const nextId = () => ++rowSeq + +// 把 JSON 字符串 {域名:{分组:系数}} 解析成扁平行 +function parseToRows(value: string): Row[] { + if (!value || !value.trim()) return [] + try { + const obj = JSON.parse(value) as Record> + const rows: Row[] = [] + for (const domain of Object.keys(obj)) { + const groupMap = obj[domain] || {} + for (const group of Object.keys(groupMap)) { + rows.push({ + id: nextId(), + domain, + group: group === '*' ? '' : group, + ratio: String(groupMap[group]), + }) + } + } + return rows + } catch { + return [] + } +} + +// 把扁平行序列化回 {域名:{分组|*:系数}},跳过非法行 +function rowsToJson(rows: Row[]): string { + const obj: Record> = {} + for (const r of rows) { + const domain = r.domain.trim().toLowerCase() + const ratio = Number(r.ratio) + if (!domain || r.ratio.trim() === '' || Number.isNaN(ratio)) continue + const group = r.group.trim() === '' ? '*' : r.group.trim() + if (!obj[domain]) obj[domain] = {} + obj[domain][group] = ratio + } + return JSON.stringify(obj) +} + +/** + * 域名计费系数编辑器:行式配置 "域名 × 命中分组 → 系数"。 + * 分组留空表示该域名默认(*)系数。最终倍率 = 模型倍率 × 命中分组倍率 × 此系数。 + */ +export function DomainRatioEditor({ value, onChange }: DomainRatioEditorProps) { + const { t } = useTranslation() + const [rows, setRows] = useState(() => parseToRows(value)) + const lastEmitted = useRef(rowsToJson(rows)) + + // 外部 value 变化(如重置/切换)且与当前不一致时,重新解析 + useEffect(() => { + try { + const incoming = value && value.trim() ? value : '{}' + const current = lastEmitted.current && lastEmitted.current.trim() ? lastEmitted.current : '{}' + if (JSON.stringify(JSON.parse(incoming)) !== JSON.stringify(JSON.parse(current))) { + setRows(parseToRows(value)) + lastEmitted.current = value + } + } catch { + // 忽略解析异常 + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + const commit = (next: Row[]) => { + setRows(next) + const json = rowsToJson(next) + lastEmitted.current = json + onChange(json) + } + + const updateRow = (id: number, patch: Partial) => { + commit(rows.map((r) => (r.id === id ? { ...r, ...patch } : r))) + } + + const addRow = () => { + commit([...rows, { id: nextId(), domain: '', group: '', ratio: '' }]) + } + + const removeRow = (id: number) => { + commit(rows.filter((r) => r.id !== id)) + } + + return ( + + + {t('Domain billing ratios')} + + {t( + 'Multiply the final group ratio by a coefficient based on the request domain (X-Forwarded-Host / Host) and the matched group. Leave group empty to apply to all groups on that domain. Final = model ratio × group ratio × this coefficient.' + )} + + + + {rows.length === 0 ? ( +

+ {t('No domain ratios configured.')} +

+ ) : ( +
+
+ {t('Domain')} + {t('Group (empty = all)')} + {t('Coefficient')} + +
+ {rows.map((r) => ( +
+ updateRow(r.id, { domain: e.target.value })} + /> + updateRow(r.id, { group: e.target.value })} + /> + updateRow(r.id, { ratio: e.target.value })} + /> + +
+ ))} +
+ )} + +
+
+ ) +} diff --git a/web/default/src/features/system-settings/models/group-ratio-form.tsx b/web/default/src/features/system-settings/models/group-ratio-form.tsx index c727adcf..c5a91bfa 100644 --- a/web/default/src/features/system-settings/models/group-ratio-form.tsx +++ b/web/default/src/features/system-settings/models/group-ratio-form.tsx @@ -58,12 +58,14 @@ import { import { SettingsPageActionsPortal } from '../components/settings-page-context' import { GroupRatioVisualEditor } from './group-ratio-visual-editor' import { GroupSpecialUsableRulesEditor } from './group-special-usable-editor' +import { DomainRatioEditor } from './domain-ratio-editor' type GroupFormValues = { GroupRatio: string TopupGroupRatio: string UserUsableGroups: string GroupGroupRatio: string + DomainRatio: string AutoGroups: string DefaultUseAutoGroup: boolean GroupSpecialUsableGroup: string @@ -153,6 +155,11 @@ export const GroupRatioForm = memo(function GroupRatioForm({ } /> + handleFieldChange('DomainRatio', value)} + /> + + ( + + {t('Domain billing ratios')} + +