Merge branch 'main' into feat_subscribe_sp1

This commit is contained in:
Little Write
2025-10-20 22:36:33 +08:00
396 changed files with 28798 additions and 5948 deletions

View File

@@ -18,7 +18,26 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useRef } from 'react';
import { Banner, Button, Form, Space, Spin } from '@douyinfe/semi-ui';
import {
Banner,
Button,
Form,
Space,
Spin,
RadioGroup,
Radio,
Table,
Modal,
Input,
Divider,
} from '@douyinfe/semi-ui';
import {
IconPlus,
IconEdit,
IconDelete,
IconSearch,
IconSaveStroked,
} from '@douyinfe/semi-icons';
import {
compareObjects,
API,
@@ -37,6 +56,52 @@ export default function SettingsChats(props) {
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
const [editMode, setEditMode] = useState('visual');
const [chatConfigs, setChatConfigs] = useState([]);
const [modalVisible, setModalVisible] = useState(false);
const [editingConfig, setEditingConfig] = useState(null);
const [isEdit, setIsEdit] = useState(false);
const [searchText, setSearchText] = useState('');
const modalFormRef = useRef();
const jsonToConfigs = (jsonString) => {
try {
const configs = JSON.parse(jsonString);
return Array.isArray(configs)
? configs.map((config, index) => ({
id: index,
name: Object.keys(config)[0] || '',
url: Object.values(config)[0] || '',
}))
: [];
} catch (error) {
console.error('JSON parse error:', error);
return [];
}
};
const configsToJson = (configs) => {
const jsonArray = configs.map((config) => ({
[config.name]: config.url,
}));
return JSON.stringify(jsonArray, null, 2);
};
const syncJsonToConfigs = () => {
const configs = jsonToConfigs(inputs.Chats);
setChatConfigs(configs);
};
const syncConfigsToJson = (configs) => {
const jsonString = configsToJson(configs);
setInputs((prev) => ({
...prev,
Chats: jsonString,
}));
if (refForm.current && editMode === 'json') {
refForm.current.setValues({ Chats: jsonString });
}
};
async function onSubmit() {
try {
@@ -103,16 +168,184 @@ export default function SettingsChats(props) {
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
if (refForm.current) {
refForm.current.setValues(currentInputs);
}
// 同步到可视化配置
const configs = jsonToConfigs(currentInputs.Chats || '[]');
setChatConfigs(configs);
}, [props.options]);
useEffect(() => {
if (editMode === 'visual') {
syncJsonToConfigs();
}
}, [inputs.Chats, editMode]);
useEffect(() => {
if (refForm.current && editMode === 'json') {
refForm.current.setValues(inputs);
}
}, [editMode, inputs]);
const handleAddConfig = () => {
setEditingConfig({ name: '', url: '' });
setIsEdit(false);
setModalVisible(true);
setTimeout(() => {
if (modalFormRef.current) {
modalFormRef.current.setValues({ name: '', url: '' });
}
}, 100);
};
const handleEditConfig = (config) => {
setEditingConfig({ ...config });
setIsEdit(true);
setModalVisible(true);
setTimeout(() => {
if (modalFormRef.current) {
modalFormRef.current.setValues(config);
}
}, 100);
};
const handleDeleteConfig = (id) => {
const newConfigs = chatConfigs.filter((config) => config.id !== id);
setChatConfigs(newConfigs);
syncConfigsToJson(newConfigs);
showSuccess(t('删除成功'));
};
const handleModalOk = () => {
if (modalFormRef.current) {
modalFormRef.current
.validate()
.then((values) => {
// 检查名称是否重复
const isDuplicate = chatConfigs.some(
(config) =>
config.name === values.name &&
(!isEdit || config.id !== editingConfig.id),
);
if (isDuplicate) {
showError(t('聊天应用名称已存在,请使用其他名称'));
return;
}
if (isEdit) {
const newConfigs = chatConfigs.map((config) =>
config.id === editingConfig.id
? { ...editingConfig, name: values.name, url: values.url }
: config,
);
setChatConfigs(newConfigs);
syncConfigsToJson(newConfigs);
} else {
const maxId =
chatConfigs.length > 0
? Math.max(...chatConfigs.map((c) => c.id))
: -1;
const newConfig = {
id: maxId + 1,
name: values.name,
url: values.url,
};
const newConfigs = [...chatConfigs, newConfig];
setChatConfigs(newConfigs);
syncConfigsToJson(newConfigs);
}
setModalVisible(false);
setEditingConfig(null);
showSuccess(isEdit ? t('编辑成功') : t('添加成功'));
})
.catch((error) => {
console.error('Modal form validation error:', error);
});
}
};
const handleModalCancel = () => {
setModalVisible(false);
setEditingConfig(null);
};
const filteredConfigs = chatConfigs.filter(
(config) =>
!searchText ||
config.name.toLowerCase().includes(searchText.toLowerCase()),
);
const highlightKeywords = (text) => {
if (!text) return text;
const parts = text.split(/(\{address\}|\{key\})/g);
return parts.map((part, index) => {
if (part === '{address}') {
return (
<span key={index} style={{ color: '#0077cc', fontWeight: 600 }}>
{part}
</span>
);
} else if (part === '{key}') {
return (
<span key={index} style={{ color: '#ff6b35', fontWeight: 600 }}>
{part}
</span>
);
}
return part;
});
};
const columns = [
{
title: t('聊天应用名称'),
dataIndex: 'name',
key: 'name',
render: (text) => text || t('未命名'),
},
{
title: t('URL链接'),
dataIndex: 'url',
key: 'url',
render: (text) => (
<div style={{ maxWidth: 300, wordBreak: 'break-all' }}>
{highlightKeywords(text)}
</div>
),
},
{
title: t('操作'),
key: 'action',
render: (_, record) => (
<Space>
<Button
type='primary'
icon={<IconEdit />}
size='small'
onClick={() => handleEditConfig(record)}
>
{t('编辑')}
</Button>
<Button
type='danger'
icon={<IconDelete />}
size='small'
onClick={() => handleDeleteConfig(record.id)}
>
{t('删除')}
</Button>
</Space>
),
},
];
return (
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Space vertical style={{ width: '100%' }}>
<Form.Section text={t('聊天设置')}>
<Banner
type='info'
@@ -120,34 +353,155 @@ export default function SettingsChats(props) {
'链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1',
)}
/>
<Form.TextArea
label={t('聊天配置')}
extraText={''}
placeholder={t('为一个 JSON 文本')}
field={'Chats'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => {
return verifyJSON(value);
},
message: t('不是合法的 JSON 字符串'),
},
]}
onChange={(value) =>
setInputs({
...inputs,
Chats: value,
})
}
/>
<Divider />
<div style={{ marginBottom: 16 }}>
<span style={{ marginRight: 16, fontWeight: 600 }}>
{t('编辑模式')}:
</span>
<RadioGroup
type='button'
value={editMode}
onChange={(e) => {
const newMode = e.target.value;
setEditMode(newMode);
// 确保模式切换时数据正确同步
setTimeout(() => {
if (newMode === 'json' && refForm.current) {
refForm.current.setValues(inputs);
}
}, 100);
}}
>
<Radio value='visual'>{t('可视化编辑')}</Radio>
<Radio value='json'>{t('JSON编辑')}</Radio>
</RadioGroup>
</div>
{editMode === 'visual' ? (
<div>
<Space style={{ marginBottom: 16 }}>
<Button
type='primary'
icon={<IconPlus />}
onClick={handleAddConfig}
>
{t('添加聊天配置')}
</Button>
<Button
type='primary'
theme='solid'
icon={<IconSaveStroked />}
onClick={onSubmit}
>
{t('保存聊天设置')}
</Button>
<Input
prefix={<IconSearch />}
placeholder={t('搜索聊天应用名称')}
value={searchText}
onChange={(value) => setSearchText(value)}
style={{ width: 250 }}
showClear
/>
</Space>
<Table
columns={columns}
dataSource={filteredConfigs}
rowKey='id'
pagination={{
pageSize: 10,
showSizeChanger: false,
showQuickJumper: true,
showTotal: (total, range) =>
t('共 {{total}} 项,当前显示 {{start}}-{{end}} 项', {
total,
start: range[0],
end: range[1],
}),
}}
/>
</div>
) : (
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
>
<Form.TextArea
label={t('聊天配置')}
extraText={''}
placeholder={t('为一个 JSON 文本')}
field={'Chats'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => {
return verifyJSON(value);
},
message: t('不是合法的 JSON 字符串'),
},
]}
onChange={(value) =>
setInputs({
...inputs,
Chats: value,
})
}
/>
</Form>
)}
</Form.Section>
</Form>
<Space>
<Button onClick={onSubmit}>{t('保存聊天设置')}</Button>
{editMode === 'json' && (
<Space>
<Button
type='primary'
icon={<IconSaveStroked />}
onClick={onSubmit}
>
{t('保存聊天设置')}
</Button>
</Space>
)}
</Space>
<Modal
title={isEdit ? t('编辑聊天配置') : t('添加聊天配置')}
visible={modalVisible}
onOk={handleModalOk}
onCancel={handleModalCancel}
width={600}
>
<Form getFormApi={(api) => (modalFormRef.current = api)}>
<Form.Input
field='name'
label={t('聊天应用名称')}
placeholder={t('请输入聊天应用名称')}
rules={[
{ required: true, message: t('请输入聊天应用名称') },
{ min: 1, message: t('名称不能为空') },
]}
/>
<Form.Input
field='url'
label={t('URL链接')}
placeholder={t('请输入完整的URL链接')}
rules={[{ required: true, message: t('请输入URL链接') }]}
/>
<Banner
type='info'
description={t(
'提示:链接中的{key}将被替换为API密钥{address}将被替换为服务器地址',
)}
style={{ marginTop: 16 }}
/>
</Form>
</Modal>
</Spin>
);
}

View File

@@ -202,9 +202,8 @@ export default function SettingClaudeModel(props) {
label={t('思考适配 BudgetTokens 百分比')}
field={'claude.thinking_adapter_budget_tokens_percentage'}
initValue={''}
extraText={t('0.1-1之间的小数')}
extraText={t('0.1以上的小数')}
min={0.1}
max={1}
onChange={(value) =>
setInputs({
...inputs,

View File

@@ -105,7 +105,7 @@ export default function SettingGlobalModel(props) {
})
}
extraText={
'开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启'
t('开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启')
}
/>
</Col>
@@ -116,7 +116,7 @@ export default function SettingGlobalModel(props) {
<Col span={24}>
<Banner
type='warning'
description='警告启用保活后如果已经写入保活数据后渠道出错系统无法重试如果必须开启推荐设置尽可能大的Ping间隔'
description={t('警告启用保活后如果已经写入保活数据后渠道出错系统无法重试如果必须开启推荐设置尽可能大的Ping间隔')}
/>
</Col>
</Row>
@@ -131,7 +131,7 @@ export default function SettingGlobalModel(props) {
'general_setting.ping_interval_enabled': value,
})
}
extraText={'开启后将定期发送ping数据保持连接活跃'}
extraText={t('开启后将定期发送ping数据保持连接活跃')}
/>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>

View File

@@ -36,6 +36,7 @@ export default function SettingsCreditLimit(props) {
PreConsumedQuota: '',
QuotaForInviter: '',
QuotaForInvitee: '',
'quota_setting.enable_free_model_pre_consume': true,
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
@@ -166,6 +167,21 @@ export default function SettingsCreditLimit(props) {
/>
</Col>
</Row>
<Row>
<Col>
<Form.Switch
label={t('对免费模型启用预消耗')}
field={'quota_setting.enable_free_model_pre_consume'}
extraText={t('开启后对免费模型倍率为0或者价格为0的模型也会预消耗额度')}
onChange={(value) =>
setInputs({
...inputs,
'quota_setting.enable_free_model_pre_consume': value,
})
}
/>
</Col>
</Row>
<Row>
<Button size='default' onClick={onSubmit}>

View File

@@ -17,8 +17,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useRef } from 'react';
import { Banner, Button, Col, Form, Row, Spin, Modal } from '@douyinfe/semi-ui';
import React, { useEffect, useState, useRef, useMemo } from 'react';
import {
Banner,
Button,
Col,
Form,
Row,
Spin,
Modal,
Select,
InputGroup,
Input,
} from '@douyinfe/semi-ui';
import {
compareObjects,
API,
@@ -35,10 +46,12 @@ export default function GeneralSettings(props) {
const [inputs, setInputs] = useState({
TopUpLink: '',
'general_setting.docs_link': '',
'general_setting.quota_display_type': 'USD',
'general_setting.custom_currency_symbol': '¤',
'general_setting.custom_currency_exchange_rate': '',
QuotaPerUnit: '',
RetryTimes: '',
USDExchangeRate: '',
DisplayInCurrencyEnabled: false,
DisplayTokenStatEnabled: false,
DefaultCollapseSidebar: false,
DemoSiteEnabled: false,
@@ -88,6 +101,30 @@ export default function GeneralSettings(props) {
});
}
// 计算展示在输入框中的“1 USD = X <currency>”中的 X
const combinedRate = useMemo(() => {
const type = inputs['general_setting.quota_display_type'];
if (type === 'USD') return '1';
if (type === 'CNY') return String(inputs['USDExchangeRate'] || '');
if (type === 'TOKENS') return String(inputs['QuotaPerUnit'] || '');
if (type === 'CUSTOM')
return String(
inputs['general_setting.custom_currency_exchange_rate'] || '',
);
return '';
}, [inputs]);
const onCombinedRateChange = (val) => {
const type = inputs['general_setting.quota_display_type'];
if (type === 'CNY') {
handleFieldChange('USDExchangeRate')(val);
} else if (type === 'TOKENS') {
handleFieldChange('QuotaPerUnit')(val);
} else if (type === 'CUSTOM') {
handleFieldChange('general_setting.custom_currency_exchange_rate')(val);
}
};
useEffect(() => {
const currentInputs = {};
for (let key in props.options) {
@@ -95,6 +132,28 @@ export default function GeneralSettings(props) {
currentInputs[key] = props.options[key];
}
}
// 若旧字段存在且新字段缺失,则做一次兜底映射
if (
currentInputs['general_setting.quota_display_type'] === undefined &&
props.options?.DisplayInCurrencyEnabled !== undefined
) {
currentInputs['general_setting.quota_display_type'] = props.options
.DisplayInCurrencyEnabled
? 'USD'
: 'TOKENS';
}
// 回填自定义货币相关字段(如果后端已存在)
if (props.options['general_setting.custom_currency_symbol'] !== undefined) {
currentInputs['general_setting.custom_currency_symbol'] =
props.options['general_setting.custom_currency_symbol'];
}
if (
props.options['general_setting.custom_currency_exchange_rate'] !==
undefined
) {
currentInputs['general_setting.custom_currency_exchange_rate'] =
props.options['general_setting.custom_currency_exchange_rate'];
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
refForm.current.setValues(currentInputs);
@@ -130,29 +189,7 @@ export default function GeneralSettings(props) {
showClear
/>
</Col>
{inputs.QuotaPerUnit !== '500000' && inputs.QuotaPerUnit !== 500000 && (
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Input
field={'QuotaPerUnit'}
label={t('单位美元额度')}
initValue={''}
placeholder={t('一单位货币能兑换的额度')}
onChange={handleFieldChange('QuotaPerUnit')}
showClear
onClick={() => setShowQuotaWarning(true)}
/>
</Col>
)}
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Input
field={'USDExchangeRate'}
label={t('美元汇率(非充值汇率,仅用于定价页面换算)')}
initValue={''}
placeholder={t('美元汇率')}
onChange={handleFieldChange('USDExchangeRate')}
showClear
/>
</Col>
{/* 单位美元额度已合入汇率组合控件TOKENS 模式下编辑),不再单独展示 */}
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Input
field={'RetryTimes'}
@@ -163,18 +200,51 @@ export default function GeneralSettings(props) {
showClear
/>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'DisplayInCurrencyEnabled'}
label={t('以货币形式显示额度')}
size='default'
checkedText=''
uncheckedText=''
onChange={handleFieldChange('DisplayInCurrencyEnabled')}
<Form.Slot label={t('站点额度展示类型及汇率')}>
<InputGroup style={{ width: '100%' }}>
<Input
prefix={'1 USD = '}
style={{ width: '50%' }}
value={combinedRate}
onChange={onCombinedRateChange}
disabled={
inputs['general_setting.quota_display_type'] === 'USD'
}
/>
<Select
style={{ width: '50%' }}
value={inputs['general_setting.quota_display_type']}
onChange={handleFieldChange(
'general_setting.quota_display_type',
)}
>
<Select.Option value='USD'>USD ($)</Select.Option>
<Select.Option value='CNY'>CNY (¥)</Select.Option>
<Select.Option value='TOKENS'>Tokens</Select.Option>
<Select.Option value='CUSTOM'>
{t('自定义货币')}
</Select.Option>
</Select>
</InputGroup>
</Form.Slot>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Input
field={'general_setting.custom_currency_symbol'}
label={t('自定义货币符号')}
placeholder={t('例如 €, £, Rp, ₩, ₹...')}
onChange={handleFieldChange(
'general_setting.custom_currency_symbol',
)}
showClear
disabled={
inputs['general_setting.quota_display_type'] !== 'CUSTOM'
}
/>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'DisplayTokenStatEnabled'}
@@ -195,8 +265,6 @@ export default function GeneralSettings(props) {
onChange={handleFieldChange('DefaultCollapseSidebar')}
/>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
field={'DemoSiteEnabled'}

View File

@@ -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 } 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 {
@@ -29,6 +38,8 @@ import {
showWarning,
} from '../../../helpers';
const { Text } = Typography;
export default function SettingsLog(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
@@ -78,24 +89,94 @@ export default function SettingsLog(props) {
});
}
async function onCleanHistoryLog() {
try {
setLoadingCleanHistoryLog(true);
if (!inputs.historyTimestamp) throw new Error(t('请选择日志记录时间'));
const res = await API.delete(
`/api/log/?target_timestamp=${Date.parse(inputs.historyTimestamp) / 1000}`,
);
const { success, message, data } = res.data;
if (success) {
showSuccess(`${data} ${t('条日志已清理!')}`);
return;
} else {
throw new Error(t('日志清理失败:') + message);
}
} catch (error) {
showError(error.message);
} finally {
setLoadingCleanHistoryLog(false);
if (!inputs.historyTimestamp) {
showError(t('请选择日志记录时间'));
return;
}
const now = dayjs();
const targetDate = dayjs(inputs.historyTimestamp);
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: (
<div style={{ lineHeight: '1.8' }}>
<p>
<Text>{t('当前时间')}</Text>
<Text strong style={{ color: '#52c41a' }}>
{currentTime}
</Text>
</p>
<p>
<Text>{t('选择时间')}</Text>
<Text strong type='danger'>
{targetTime}
</Text>
{daysDiff > 0 && (
<Text type='tertiary'>
{' '}
({t('约')} {daysDiff} {t('天前')})
</Text>
)}
</p>
<div
style={{
background: '#fff7e6',
border: '1px solid #ffd591',
padding: '12px',
borderRadius: '4px',
marginTop: '12px',
color: '#333',
}}
>
<Text strong style={{ color: '#d46b08' }}>
{t('注意')}
</Text>
<Text style={{ color: '#333' }}>{t('将删除')} </Text>
<Text strong style={{ color: '#cf1322' }}>
{targetTime}
</Text>
{daysDiff > 0 && (
<Text style={{ color: '#8c8c8c' }}>
{' '}
({t('约')} {daysDiff} {t('天前')})
</Text>
)}
<Text style={{ color: '#333' }}> {t('之前的所有日志')}</Text>
</div>
<p style={{ marginTop: '12px' }}>
<Text type='danger'>
{t('此操作不可恢复,请仔细确认时间后再操作!')}
</Text>
</p>
</div>
),
okText: t('确认删除'),
cancelText: t('取消'),
okType: 'danger',
onOk: async () => {
try {
setLoadingCleanHistoryLog(true);
const res = await API.delete(
`/api/log/?target_timestamp=${Date.parse(inputs.historyTimestamp) / 1000}`,
);
const { success, message, data } = res.data;
if (success) {
showSuccess(`${data} ${t('条日志已清理!')}`);
return;
} else {
throw new Error(t('日志清理失败:') + message);
}
} catch (error) {
showError(error.message);
} finally {
setLoadingCleanHistoryLog(false);
}
},
});
}
useEffect(() => {
@@ -138,7 +219,7 @@ export default function SettingsLog(props) {
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Spin spinning={loadingCleanHistoryLog}>
<Form.DatePicker
label={t('日志记录时间')}
label={t('清除历史日志')}
field={'historyTimestamp'}
type='dateTime'
inputReadOnly={true}
@@ -149,7 +230,18 @@ export default function SettingsLog(props) {
});
}}
/>
<Button size='default' onClick={onCleanHistoryLog}>
<Text
type='tertiary'
size='small'
style={{ display: 'block', marginTop: 4, marginBottom: 8 }}
>
{t('将清除选定时间之前的所有日志')}
</Text>
<Button
size='default'
type='danger'
onClick={onCleanHistoryLog}
>
{t('清除历史日志')}
</Button>
</Spin>

View File

@@ -128,7 +128,8 @@ export default function SettingsMonitoring(props) {
onChange={(value) =>
setInputs({
...inputs,
'monitor_setting.auto_test_channel_minutes': parseInt(value),
'monitor_setting.auto_test_channel_minutes':
parseInt(value),
})
}
/>

View File

@@ -118,14 +118,20 @@ export default function SettingsPaymentGateway(props) {
}
}
if (originInputs['AmountOptions'] !== inputs.AmountOptions && inputs.AmountOptions.trim() !== '') {
if (
originInputs['AmountOptions'] !== inputs.AmountOptions &&
inputs.AmountOptions.trim() !== ''
) {
if (!verifyJSON(inputs.AmountOptions)) {
showError(t('自定义充值数量选项不是合法的 JSON 数组'));
return;
}
}
if (originInputs['AmountDiscount'] !== inputs.AmountDiscount && inputs.AmountDiscount.trim() !== '') {
if (
originInputs['AmountDiscount'] !== inputs.AmountDiscount &&
inputs.AmountDiscount.trim() !== ''
) {
if (!verifyJSON(inputs.AmountDiscount)) {
showError(t('充值金额折扣配置不是合法的 JSON 对象'));
return;
@@ -163,10 +169,16 @@ export default function SettingsPaymentGateway(props) {
options.push({ key: 'PayMethods', value: inputs.PayMethods });
}
if (originInputs['AmountOptions'] !== inputs.AmountOptions) {
options.push({ key: 'payment_setting.amount_options', value: inputs.AmountOptions });
options.push({
key: 'payment_setting.amount_options',
value: inputs.AmountOptions,
});
}
if (originInputs['AmountDiscount'] !== inputs.AmountDiscount) {
options.push({ key: 'payment_setting.amount_discount', value: inputs.AmountDiscount });
options.push({
key: 'payment_setting.amount_discount',
value: inputs.AmountDiscount,
});
}
// 发送请求
@@ -273,7 +285,7 @@ export default function SettingsPaymentGateway(props) {
placeholder={t('为一个 JSON 文本')}
autosize
/>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
@@ -282,13 +294,17 @@ export default function SettingsPaymentGateway(props) {
<Form.TextArea
field='AmountOptions'
label={t('自定义充值数量选项')}
placeholder={t('为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]')}
placeholder={t(
'为一个 JSON 数组,例如:[10, 20, 50, 100, 200, 500]',
)}
autosize
extraText={t('设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]')}
extraText={t(
'设置用户可选择的充值数量选项,例如:[10, 20, 50, 100, 200, 500]',
)}
/>
</Col>
</Row>
<Row
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
style={{ marginTop: 16 }}
@@ -297,13 +313,17 @@ export default function SettingsPaymentGateway(props) {
<Form.TextArea
field='AmountDiscount'
label={t('充值金额折扣配置')}
placeholder={t('为一个 JSON 对象,例如:{"100": 0.95, "200": 0.9, "500": 0.85}')}
placeholder={t(
'为一个 JSON 对象,例如:{"100": 0.95, "200": 0.9, "500": 0.85}',
)}
autosize
extraText={t('设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{"100": 0.95, "200": 0.9, "500": 0.85}')}
extraText={t(
'设置不同充值金额对应的折扣,键为充值金额,值为折扣率,例如:{"100": 0.95, "200": 0.9, "500": 0.85}',
)}
/>
</Col>
</Row>
<Button onClick={submitPayAddress}>{t('更新支付设置')}</Button>
</Form.Section>
</Form>

View File

@@ -45,6 +45,7 @@ export default function SettingsPaymentGateway(props) {
StripePriceId: '',
StripeUnitPrice: 8.0,
StripeMinTopUp: 1,
StripePromotionCodesEnabled: false,
});
const [originInputs, setOriginInputs] = useState({});
const formApiRef = useRef(null);
@@ -63,6 +64,10 @@ export default function SettingsPaymentGateway(props) {
props.options.StripeMinTopUp !== undefined
? parseFloat(props.options.StripeMinTopUp)
: 1,
StripePromotionCodesEnabled:
props.options.StripePromotionCodesEnabled !== undefined
? props.options.StripePromotionCodesEnabled
: false,
};
setInputs(currentInputs);
setOriginInputs({ ...currentInputs });
@@ -114,6 +119,16 @@ export default function SettingsPaymentGateway(props) {
value: inputs.StripeMinTopUp.toString(),
});
}
if (
originInputs['StripePromotionCodesEnabled'] !==
inputs.StripePromotionCodesEnabled &&
inputs.StripePromotionCodesEnabled !== undefined
) {
options.push({
key: 'StripePromotionCodesEnabled',
value: inputs.StripePromotionCodesEnabled ? 'true' : 'false',
});
}
// 发送请求
const requestQueue = options.map((opt) =>
@@ -225,6 +240,15 @@ export default function SettingsPaymentGateway(props) {
placeholder={t('例如2就是最低充值2$')}
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Switch
field='StripePromotionCodesEnabled'
size='default'
checkedText=''
uncheckedText=''
label={t('允许在 Stripe 支付中输入促销码')}
/>
</Col>
</Row>
<Button onClick={submitStripeSetting}>{t('更新 Stripe 设置')}</Button>
</Form.Section>

View File

@@ -226,8 +226,12 @@ export default function ModelRatioSettings(props) {
<Col xs={24} sm={16}>
<Form.TextArea
label={t('图片输入倍率(仅部分模型支持该计费)')}
extraText={t('图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-image-1": 2}')}
extraText={t(
'图片输入相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费',
)}
placeholder={t(
'为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-image-1": 2}',
)}
field={'ImageRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
@@ -238,9 +242,7 @@ export default function ModelRatioSettings(props) {
message: '不是合法的 JSON 字符串',
},
]}
onChange={(value) =>
setInputs({ ...inputs, ImageRatio: value })
}
onChange={(value) => setInputs({ ...inputs, ImageRatio: value })}
/>
</Col>
</Row>
@@ -249,7 +251,9 @@ export default function ModelRatioSettings(props) {
<Form.TextArea
label={t('音频倍率(仅部分模型支持该计费)')}
extraText={t('音频输入相关的倍率设置,键为模型名称,值为倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-4o-audio-preview": 16}')}
placeholder={t(
'为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-4o-audio-preview": 16}',
)}
field={'AudioRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
@@ -260,9 +264,7 @@ export default function ModelRatioSettings(props) {
message: '不是合法的 JSON 字符串',
},
]}
onChange={(value) =>
setInputs({ ...inputs, AudioRatio: value })
}
onChange={(value) => setInputs({ ...inputs, AudioRatio: value })}
/>
</Col>
</Row>
@@ -270,8 +272,12 @@ export default function ModelRatioSettings(props) {
<Col xs={24} sm={16}>
<Form.TextArea
label={t('音频补全倍率(仅部分模型支持该计费)')}
extraText={t('音频输出补全相关的倍率设置,键为模型名称,值为倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-4o-realtime": 2}')}
extraText={t(
'音频输出补全相关的倍率设置,键为模型名称,值为倍率',
)}
placeholder={t(
'为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-4o-realtime": 2}',
)}
field={'AudioCompletionRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'

View File

@@ -108,7 +108,7 @@ const Setting = () => {
tab: (
<span style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<Calculator size={18} />
{t('倍率设置')}
{t('分组与模型定价设置')}
</span>
),
content: <RatioSetting />,