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 })} + /> + removeRow(r.id)} + > + + + + ))} + + )} + + + {t('Add domain ratio')} + + + + ) +} 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')} + + + + + {t( + 'Nested JSON: domain → { group | "*": coefficient }. Multiplies the final group ratio by domain × matched group. Example:' + )}{' '} + {`{ "api1.example.com": { "*": 1.2 }, "api2.example.com": { "*": 1.5, "vip": 2 } }`} + + + + )} + /> + { + const result = validateJsonString(value) + if (!result.valid) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: result.message || 'Invalid JSON', + }) + } + }), AutoGroups: z.string().superRefine((value, ctx) => { const result = validateJsonString(value, { predicate: (parsed) => @@ -292,6 +301,7 @@ export function RatioSettingsCard({ TopupGroupRatio: formatJsonForTextarea(groupDefaults.TopupGroupRatio), UserUsableGroups: formatJsonForTextarea(groupDefaults.UserUsableGroups), GroupGroupRatio: formatJsonForTextarea(groupDefaults.GroupGroupRatio), + DomainRatio: formatJsonForTextarea(groupDefaults.DomainRatio), AutoGroups: formatJsonForTextarea(groupDefaults.AutoGroups), GroupSpecialUsableGroup: formatJsonForTextarea( groupDefaults.GroupSpecialUsableGroup @@ -339,6 +349,7 @@ export function RatioSettingsCard({ TopupGroupRatio: normalizeJsonString(groupDefaults.TopupGroupRatio), UserUsableGroups: normalizeJsonString(groupDefaults.UserUsableGroups), GroupGroupRatio: normalizeJsonString(groupDefaults.GroupGroupRatio), + DomainRatio: normalizeJsonString(groupDefaults.DomainRatio), AutoGroups: normalizeJsonString(groupDefaults.AutoGroups), DefaultUseAutoGroup: groupDefaults.DefaultUseAutoGroup, GroupSpecialUsableGroup: normalizeJsonString( @@ -352,6 +363,7 @@ export function RatioSettingsCard({ TopupGroupRatio: formatJsonForTextarea(groupDefaults.TopupGroupRatio), UserUsableGroups: formatJsonForTextarea(groupDefaults.UserUsableGroups), GroupGroupRatio: formatJsonForTextarea(groupDefaults.GroupGroupRatio), + DomainRatio: formatJsonForTextarea(groupDefaults.DomainRatio), AutoGroups: formatJsonForTextarea(groupDefaults.AutoGroups), GroupSpecialUsableGroup: formatJsonForTextarea( groupDefaults.GroupSpecialUsableGroup @@ -406,6 +418,7 @@ export function RatioSettingsCard({ TopupGroupRatio: normalizeJsonString(values.TopupGroupRatio), UserUsableGroups: normalizeJsonString(values.UserUsableGroups), GroupGroupRatio: normalizeJsonString(values.GroupGroupRatio), + DomainRatio: normalizeJsonString(values.DomainRatio), AutoGroups: normalizeJsonString(values.AutoGroups), DefaultUseAutoGroup: values.DefaultUseAutoGroup, GroupSpecialUsableGroup: normalizeJsonString( diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 1ce821f5..2bab071f 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -439,6 +439,13 @@ "Auto": "自动", "Auto (Circuit Breaker)": "自动分组(熔断)", "Auto assignment order": "自动分配顺序", + "Domain billing ratios": "域名计费系数", + "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.": "根据请求域名(X-Forwarded-Host / Host)与实际命中的分组,给最终分组倍率再乘一个系数。分组留空表示对该域名所有分组生效。最终 = 模型倍率 × 分组倍率 × 此系数。", + "No domain ratios configured.": "尚未配置域名系数。", + "Group (empty = all)": "分组(留空=全部)", + "Coefficient": "系数", + "Add domain ratio": "添加域名系数", + "Nested JSON: domain → { group | \"*\": coefficient }. Multiplies the final group ratio by domain × matched group. Example:": "嵌套 JSON:域名 → { 分组 | \"*\": 系数 }。按 域名×命中分组 给最终分组倍率乘系数。示例:", "Auto Ban": "自动封禁", "Auto detect (default)": "自动检测(默认)", "Auto Disabled": "自动禁用",
+ {t('No domain ratios configured.')} +