创建日期:2026-06-04
状态:待实施
相关组件:ModelSettingsVisualEditor.jsx
在现有的 ModelSettingsVisualEditor 组件中添加高级倍率字段支持,允许用户为每个模型单独设置缓存、图片、音频相关的倍率。
现有的可视化倍率设置只能设置:
新增以下5个高级倍率字段:
表格列结构保持现有的简洁布局:
| 列名 | 说明 |
|---|---|
| 模型名称 | 显示模型名称,如有矛盾显示红色标签 |
| 模型固定价格 | 按次计费的价格输入框 |
| 模型倍率 | 按量计费的模型倍率 |
| 补全倍率 | 按量计费的补全倍率 |
| 操作 | 编辑、删除按钮 |
在现有的表单结构中,在"定价模式"和"价格设置方式"区域之后,增加一个新的表单区域:
{pricingMode === 'per-token' && (
<>
{/* 现有的价格设置方式区域 */}
<Form.Section text={t('价格设置方式')}>
...
</Form.Section>
{/* 新增:高级比例区域 */}
<Form.Section text={t('高级比例(留空使用默认值)')}>
<Form.InputNumber
field="cacheRatio"
label={t('缓存读取倍率')}
min={0}
step={0.01}
placeholder={t('默认值:1.0')}
/>
<Form.InputNumber
field="cacheCreationRatio"
label={t('缓存创建倍率(5分钟)')}
min={0}
step={0.01}
placeholder={t('默认值:1.25')}
/>
<Form.InputNumber
field="imageRatio"
label={t('图片倍率')}
min={0}
step={0.01}
placeholder={t('默认值:1.0')}
/>
<Form.InputNumber
field="audioRatio"
label={t('音频输入倍率')}
min={0}
step={0.01}
placeholder={t('默认值:1.0')}
/>
<Form.InputNumber
field="audioCompletionRatio"
label={t('音频输出倍率')}
min={0}
step={0.01}
placeholder={t('默认值:1.0')}
/>
</Form.Section>
</>
)}
设计决策:
Form.InputNumber 确保输入的是有效数字placeholder 显示默认值提示step={0.01} 允许小数点后两位精度在 models 状态数组中,每个模型对象增加5个字段:
{
name: 'gpt-4',
price: '',
ratio: '',
completionRatio: '',
cacheRatio: '', // 新增
cacheCreationRatio: '', // 新增
imageRatio: '', // 新增
audioRatio: '', // 新增
audioCompletionRatio: '', // 新增
hasConflict: false
}
useEffect(() => {
try {
const modelPrice = JSON.parse(props.options.ModelPrice || '{}');
const modelRatio = JSON.parse(props.options.ModelRatio || '{}');
const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
// 新增:解析高级倍率字段
const cacheRatio = JSON.parse(props.options.CacheRatio || '{}');
const createCacheRatio = JSON.parse(props.options.CreateCacheRatio || '{}');
const imageRatio = JSON.parse(props.options.ImageRatio || '{}');
const audioRatio = JSON.parse(props.options.AudioRatio || '{}');
const audioCompletionRatio = JSON.parse(props.options.AudioCompletionRatio || '{}');
// 合并所有模型名称
const modelNames = new Set([
...Object.keys(modelPrice),
...Object.keys(modelRatio),
...Object.keys(completionRatio),
...Object.keys(cacheRatio),
...Object.keys(createCacheRatio),
...Object.keys(imageRatio),
...Object.keys(audioRatio),
...Object.keys(audioCompletionRatio),
]);
const modelData = Array.from(modelNames).map((name) => {
return {
name,
price: modelPrice[name] === undefined ? '' : modelPrice[name],
ratio: modelRatio[name] === undefined ? '' : modelRatio[name],
completionRatio: completionRatio[name] === undefined ? '' : completionRatio[name],
cacheRatio: cacheRatio[name] === undefined ? '' : cacheRatio[name],
cacheCreationRatio: createCacheRatio[name] === undefined ? '' : createCacheRatio[name],
imageRatio: imageRatio[name] === undefined ? '' : imageRatio[name],
audioRatio: audioRatio[name] === undefined ? '' : audioRatio[name],
audioCompletionRatio: audioCompletionRatio[name] === undefined ? '' : audioCompletionRatio[name],
hasConflict: modelPrice[name] !== '' && (modelRatio[name] !== '' || completionRatio[name] !== ''),
};
});
setModels(modelData);
} catch (error) {
console.error('JSON解析错误:', error);
}
}, [props.options]);
保存时将5个新增字段序列化到对应的 JSON 字段:
const SubmitData = async () => {
setLoading(true);
const output = {
ModelPrice: {},
ModelRatio: {},
CompletionRatio: {},
CacheRatio: {}, // 新增
CreateCacheRatio: {}, // 新增
ImageRatio: {}, // 新增
AudioRatio: {}, // 新增
AudioCompletionRatio: {} // 新增
};
try {
// 数据转换
models.forEach((model) => {
if (model.price !== '') {
output.ModelPrice[model.name] = parseFloat(model.price);
} else {
if (model.ratio !== '')
output.ModelRatio[model.name] = parseFloat(model.ratio);
if (model.completionRatio !== '')
output.CompletionRatio[model.name] = parseFloat(model.completionRatio);
}
// 新增:处理高级倍率字段
if (model.cacheRatio !== '')
output.CacheRatio[model.name] = parseFloat(model.cacheRatio);
if (model.cacheCreationRatio !== '')
output.CreateCacheRatio[model.name] = parseFloat(model.cacheCreationRatio);
if (model.imageRatio !== '')
output.ImageRatio[model.name] = parseFloat(model.imageRatio);
if (model.audioRatio !== '')
output.AudioRatio[model.name] = parseFloat(model.audioRatio);
if (model.audioCompletionRatio !== '')
output.AudioCompletionRatio[model.name] = parseFloat(model.audioCompletionRatio);
});
// 准备API请求数组
const finalOutput = {
ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2),
CacheRatio: JSON.stringify(output.CacheRatio, null, 2),
CreateCacheRatio: JSON.stringify(output.CreateCacheRatio, null, 2),
ImageRatio: JSON.stringify(output.ImageRatio, null, 2),
AudioRatio: JSON.stringify(output.AudioRatio, null, 2),
AudioCompletionRatio: JSON.stringify(output.AudioCompletionRatio, null, 2),
};
const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
return API.put('/api/option/', { key, value });
});
// 批量处理请求
const results = await Promise.all(requestQueue);
// 验证结果(保持现有逻辑)
// ...
} catch (error) {
// 错误处理
} finally {
setLoading(false);
}
};
编辑现有模型时,需要在弹窗中正确回显高级倍率字段的值:
const editModel = (record) => {
setIsEditMode(true);
// 现有的定价模式设置逻辑
// ...
// 创建模型副本
const modelCopy = { ...record };
// 如果有倍率数据,转换为 token price(现有逻辑)
if (record.ratio) {
modelCopy.tokenPrice = calculateTokenPriceFromRatio(parseFloat(record.ratio)).toString();
if (record.completionRatio) {
modelCopy.completionTokenPrice = (
parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio)
).toString();
}
}
// 设置当前模型
setCurrentModel(modelCopy);
setVisible(true);
// 设置表单值
setTimeout(() => {
if (formRef.current) {
formRef.current.setValues({
name: modelCopy.name,
// 现有字段
ratioInput: modelCopy.ratio,
completionRatioInput: modelCopy.completionRatio,
modelTokenPrice: modelCopy.tokenPrice,
completionTokenPrice: modelCopy.completionTokenPrice,
// 新增:高级倍率字段
cacheRatio: modelCopy.cacheRatio,
cacheCreationRatio: modelCopy.cacheCreationRatio,
imageRatio: modelCopy.imageRatio,
audioRatio: modelCopy.audioRatio,
audioCompletionRatio: modelCopy.audioCompletionRatio,
});
}
}, 0);
};
在 addOrUpdateModel 函数中处理高级倍率字段:
const addOrUpdateModel = (values) => {
const existingModelIndex = models.findIndex(
(model) => model.name === values.name,
);
const updatedModel = {
name: values.name,
price: values.price || '',
ratio: values.ratio || '',
completionRatio: values.completionRatio || '',
// 新增:高级倍率字段
cacheRatio: values.cacheRatio || '',
cacheCreationRatio: values.cacheCreationRatio || '',
imageRatio: values.imageRatio || '',
audioRatio: values.audioRatio || '',
audioCompletionRatio: values.audioCompletionRatio || '',
};
updatedModel.hasConflict =
updatedModel.price !== '' &&
(updatedModel.ratio !== '' || updatedModel.completionRatio !== '');
if (existingModelIndex >= 0) {
// 更新现有模型
setModels((prev) =>
prev.map((model, index) =>
index !== existingModelIndex ? model : updatedModel
)
);
setVisible(false);
showSuccess(t('更新成功'));
} else {
// 添加新模型
if (models.some((model) => model.name === values.name)) {
showError(t('模型名称已存在'));
return;
}
setModels((prev) => [updatedModel, ...prev]);
setVisible(false);
showSuccess(t('添加成功'));
}
};
在 updateModel 函数中增加对高级倍率字段的验证:
const updateModel = (name, field, value) => {
if (isNaN(value) && value !== '') { // 允许空字符串
showError(t('请输入数字'));
return;
}
setModels((prev) =>
prev.map((model) => {
if (model.name !== name) return model;
const updated = { ...model, [field]: value };
updated.hasConflict =
updated.price !== '' &&
(updated.ratio !== '' || updated.completionRatio !== '');
return updated;
}),
);
};
前端处理默认值:用户输入框留空时,保存为空字符串,后端使用全局默认值。
默认值映射:
实现方式:
placeholder 提示默认值策略:全部显示,用户自行判断
设计理由:
确保前端字段名与后端 options 表中的 key 一致:
| 前端字段名 | 后端 Option Key | 默认值 |
|---|---|---|
| cacheRatio | CacheRatio | 1.0 |
| cacheCreationRatio | CreateCacheRatio | 1.25 |
| imageRatio | ImageRatio | 1.0 |
| audioRatio | AudioRatio | 1.0 |
| audioCompletionRatio | AudioCompletionRatio | 1.0 |
if (isNaN(value) && value !== '')min={0} 确保非负数step={0.01} 允许两位小数cacheRatio[name] === undefined ? '' : cacheRatio[name]Form.InputNumber 组件label、placeholder 样式Form.Section 包裹,标题为"高级比例(留空使用默认值)”需要为新增的文本添加翻译:
中文 (zh-CN.json):
{
"高级比例(留空使用默认值)": "高级比例(留空使用默认值)",
"缓存读取倍率": "缓存读取倍率",
"缓存创建倍率(5分钟)": "缓存创建倍率(5分钟)",
"图片倍率": "图片倍率",
"音频输入倍率": "音频输入倍率",
"音频输出倍率": "音频输出倍率",
"默认值:1.0": "默认值:1.0",
"默认值:1.25": "默认值:1.25"
}
英文 (en.json):
{
"高级比例(留空使用默认值)": "Advanced Ratios (Leave empty for defaults)",
"缓存读取倍率": "Cache Read Ratio",
"缓存创建倍率(5分钟)": "Cache Creation Ratio (5min)",
"图片倍率": "Image Ratio",
"音频输入倍率": "Audio Input Ratio",
"音频输出倍率": "Audio Output Ratio",
"默认值:1.0": "Default: 1.0",
"默认值:1.25": "Default: 1.25"
}
本设计参考了 feat/channel-pricing-extended 分支中的 ChannelPricingView.jsx 实现,特别是:
Form.Section 包裹高级比例字段Form.InputNumber 配合 min 和 step 属性