Merge branch 'alpha' into refactor/model-pricing
This commit is contained in:
609
web/src/components/common/JSONEditor.js
Normal file
609
web/src/components/common/JSONEditor.js
Normal file
@@ -0,0 +1,609 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Space,
|
||||
Button,
|
||||
Form,
|
||||
Card,
|
||||
Typography,
|
||||
Banner,
|
||||
Row,
|
||||
Col,
|
||||
InputNumber,
|
||||
Switch,
|
||||
Select,
|
||||
Input,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconCode,
|
||||
IconEdit,
|
||||
IconPlus,
|
||||
IconDelete,
|
||||
IconSetting,
|
||||
} from '@douyinfe/semi-icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const JSONEditor = ({
|
||||
value = '',
|
||||
onChange,
|
||||
field,
|
||||
label,
|
||||
placeholder,
|
||||
extraText,
|
||||
showClear = true,
|
||||
template,
|
||||
templateLabel,
|
||||
editorType = 'keyValue', // keyValue, object, region
|
||||
autosize = true,
|
||||
rules = [],
|
||||
formApi = null,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 初始化JSON数据
|
||||
const [jsonData, setJsonData] = useState(() => {
|
||||
// 初始化时解析JSON数据
|
||||
if (value && value.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
// 根据键数量决定默认编辑模式
|
||||
const [editMode, setEditMode] = useState(() => {
|
||||
// 如果初始JSON数据的键数量大于10个,则默认使用手动模式
|
||||
if (value && value.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
const keyCount = Object.keys(parsed).length;
|
||||
return keyCount > 10 ? 'manual' : 'visual';
|
||||
} catch (error) {
|
||||
return 'visual';
|
||||
}
|
||||
}
|
||||
return 'visual';
|
||||
});
|
||||
const [jsonError, setJsonError] = useState('');
|
||||
|
||||
// 数据同步 - 当value变化时总是更新jsonData(如果JSON有效)
|
||||
useEffect(() => {
|
||||
try {
|
||||
const parsed = value && value.trim() ? JSON.parse(value) : {};
|
||||
setJsonData(parsed);
|
||||
setJsonError('');
|
||||
} catch (error) {
|
||||
console.log('JSON解析失败:', error.message);
|
||||
setJsonError(error.message);
|
||||
// JSON格式错误时不更新jsonData
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
|
||||
// 处理可视化编辑的数据变化
|
||||
const handleVisualChange = useCallback((newData) => {
|
||||
setJsonData(newData);
|
||||
setJsonError('');
|
||||
const jsonString = Object.keys(newData).length === 0 ? '' : JSON.stringify(newData, null, 2);
|
||||
|
||||
// 通过formApi设置值(如果提供的话)
|
||||
if (formApi && field) {
|
||||
formApi.setValue(field, jsonString);
|
||||
}
|
||||
|
||||
onChange?.(jsonString);
|
||||
}, [onChange, formApi, field]);
|
||||
|
||||
// 处理手动编辑的数据变化
|
||||
const handleManualChange = useCallback((newValue) => {
|
||||
onChange?.(newValue);
|
||||
// 验证JSON格式
|
||||
if (newValue && newValue.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(newValue);
|
||||
setJsonError('');
|
||||
// 预先准备可视化数据,但不立即应用
|
||||
// 这样切换到可视化模式时数据已经准备好了
|
||||
} catch (error) {
|
||||
setJsonError(error.message);
|
||||
}
|
||||
} else {
|
||||
setJsonError('');
|
||||
}
|
||||
}, [onChange]);
|
||||
|
||||
// 切换编辑模式
|
||||
const toggleEditMode = useCallback(() => {
|
||||
if (editMode === 'visual') {
|
||||
// 从可视化模式切换到手动模式
|
||||
setEditMode('manual');
|
||||
} else {
|
||||
// 从手动模式切换到可视化模式,需要验证JSON
|
||||
try {
|
||||
const parsed = value && value.trim() ? JSON.parse(value) : {};
|
||||
setJsonData(parsed);
|
||||
setJsonError('');
|
||||
setEditMode('visual');
|
||||
} catch (error) {
|
||||
setJsonError(error.message);
|
||||
// JSON格式错误时不切换模式
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [editMode, value]);
|
||||
|
||||
// 添加键值对
|
||||
const addKeyValue = useCallback(() => {
|
||||
const newData = { ...jsonData };
|
||||
const keys = Object.keys(newData);
|
||||
let newKey = 'key';
|
||||
let counter = 1;
|
||||
while (newData.hasOwnProperty(newKey)) {
|
||||
newKey = `key${counter}`;
|
||||
counter++;
|
||||
}
|
||||
newData[newKey] = '';
|
||||
handleVisualChange(newData);
|
||||
}, [jsonData, handleVisualChange]);
|
||||
|
||||
// 删除键值对
|
||||
const removeKeyValue = useCallback((keyToRemove) => {
|
||||
const newData = { ...jsonData };
|
||||
delete newData[keyToRemove];
|
||||
handleVisualChange(newData);
|
||||
}, [jsonData, handleVisualChange]);
|
||||
|
||||
// 更新键名
|
||||
const updateKey = useCallback((oldKey, newKey) => {
|
||||
if (oldKey === newKey) return;
|
||||
const newData = { ...jsonData };
|
||||
const value = newData[oldKey];
|
||||
delete newData[oldKey];
|
||||
newData[newKey] = value;
|
||||
handleVisualChange(newData);
|
||||
}, [jsonData, handleVisualChange]);
|
||||
|
||||
// 更新值
|
||||
const updateValue = useCallback((key, newValue) => {
|
||||
const newData = { ...jsonData };
|
||||
newData[key] = newValue;
|
||||
handleVisualChange(newData);
|
||||
}, [jsonData, handleVisualChange]);
|
||||
|
||||
// 填入模板
|
||||
const fillTemplate = useCallback(() => {
|
||||
if (template) {
|
||||
const templateString = JSON.stringify(template, null, 2);
|
||||
|
||||
// 通过formApi设置值(如果提供的话)
|
||||
if (formApi && field) {
|
||||
formApi.setValue(field, templateString);
|
||||
}
|
||||
|
||||
// 无论哪种模式都要更新值
|
||||
onChange?.(templateString);
|
||||
|
||||
// 如果是可视化模式,同时更新jsonData
|
||||
if (editMode === 'visual') {
|
||||
setJsonData(template);
|
||||
}
|
||||
|
||||
// 清除错误状态
|
||||
setJsonError('');
|
||||
}
|
||||
}, [template, onChange, editMode, formApi, field]);
|
||||
|
||||
// 渲染键值对编辑器
|
||||
const renderKeyValueEditor = () => {
|
||||
const entries = Object.entries(jsonData);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{entries.length === 0 && (
|
||||
<div className="text-center py-6 px-4">
|
||||
<div className="text-gray-400 mb-2">
|
||||
<IconCode size={32} />
|
||||
</div>
|
||||
<Text type="tertiary" className="text-gray-500 text-sm">
|
||||
{t('暂无数据,点击下方按钮添加键值对')}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entries.map(([key, value], index) => (
|
||||
<Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
|
||||
<Row gutter={12} align="middle">
|
||||
<Col span={10}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('键名')}</Text>
|
||||
<Input
|
||||
placeholder={t('键名')}
|
||||
value={key}
|
||||
onChange={(newKey) => updateKey(key, newKey)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('值')}</Text>
|
||||
<Input
|
||||
placeholder={t('值')}
|
||||
value={value}
|
||||
onChange={(newValue) => updateValue(key, newValue)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
onClick={() => removeKeyValue(key)}
|
||||
className="hover:bg-red-50"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<div className="flex justify-center pt-1">
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={addKeyValue}
|
||||
size="small"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
className="shadow-sm hover:shadow-md transition-shadow px-4"
|
||||
>
|
||||
{t('添加键值对')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染对象编辑器(用于复杂JSON)
|
||||
const renderObjectEditor = () => {
|
||||
const entries = Object.entries(jsonData);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{entries.length === 0 && (
|
||||
<div className="text-center py-6 px-4">
|
||||
<div className="text-gray-400 mb-2">
|
||||
<IconSetting size={32} />
|
||||
</div>
|
||||
<Text type="tertiary" className="text-gray-500 text-sm">
|
||||
{t('暂无参数,点击下方按钮添加请求参数')}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entries.map(([key, value], index) => (
|
||||
<Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
|
||||
<Row gutter={12} align="middle">
|
||||
<Col span={8}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('参数名')}</Text>
|
||||
<Input
|
||||
placeholder={t('参数名')}
|
||||
value={key}
|
||||
onChange={(newKey) => updateKey(key, newKey)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={13}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('参数值')} ({typeof value})</Text>
|
||||
{renderValueInput(key, value)}
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
onClick={() => removeKeyValue(key)}
|
||||
className="hover:bg-red-50"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<div className="flex justify-center pt-1">
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={addKeyValue}
|
||||
size="small"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
className="shadow-sm hover:shadow-md transition-shadow px-4"
|
||||
>
|
||||
{t('添加参数')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染参数值输入控件
|
||||
const renderValueInput = (key, value) => {
|
||||
const valueType = typeof value;
|
||||
|
||||
if (valueType === 'boolean') {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<Switch
|
||||
checked={value}
|
||||
onChange={(newValue) => updateValue(key, newValue)}
|
||||
size="small"
|
||||
/>
|
||||
<Text type="tertiary" size="small" className="ml-2">
|
||||
{value ? t('true') : t('false')}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (valueType === 'number') {
|
||||
return (
|
||||
<InputNumber
|
||||
value={value}
|
||||
onChange={(newValue) => updateValue(key, newValue)}
|
||||
size="small"
|
||||
style={{ width: '100%' }}
|
||||
step={key === 'temperature' ? 0.1 : 1}
|
||||
precision={key === 'temperature' ? 2 : 0}
|
||||
placeholder={t('输入数字')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 字符串类型或其他类型
|
||||
return (
|
||||
<Input
|
||||
placeholder={t('参数值')}
|
||||
value={String(value)}
|
||||
onChange={(newValue) => {
|
||||
// 尝试转换为适当的类型
|
||||
let convertedValue = newValue;
|
||||
if (newValue === 'true') convertedValue = true;
|
||||
else if (newValue === 'false') convertedValue = false;
|
||||
else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
|
||||
convertedValue = Number(newValue);
|
||||
}
|
||||
|
||||
updateValue(key, convertedValue);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染区域编辑器(特殊格式)
|
||||
const renderRegionEditor = () => {
|
||||
const entries = Object.entries(jsonData);
|
||||
const defaultEntry = entries.find(([key]) => key === 'default');
|
||||
const modelEntries = entries.filter(([key]) => key !== 'default');
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{/* 默认区域 */}
|
||||
<Card className="!p-2 !border-blue-200 !bg-blue-50">
|
||||
<div className="flex items-center mb-1">
|
||||
<Text strong size="small" className="text-blue-700">{t('默认区域')}</Text>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t('默认区域,如: us-central1')}
|
||||
value={defaultEntry ? defaultEntry[1] : ''}
|
||||
onChange={(value) => updateValue('default', value)}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 模型专用区域 */}
|
||||
<div className="space-y-1">
|
||||
<Text strong size="small">{t('模型专用区域')}</Text>
|
||||
{modelEntries.map(([modelName, region], index) => (
|
||||
<Card key={index} className="!p-3 !border-gray-200 !rounded-md hover:shadow-sm transition-shadow duration-200">
|
||||
<Row gutter={12} align="middle">
|
||||
<Col span={10}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('模型名称')}</Text>
|
||||
<Input
|
||||
placeholder={t('模型名称')}
|
||||
value={modelName}
|
||||
onChange={(newKey) => updateKey(modelName, newKey)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<div className="space-y-1">
|
||||
<Text type="tertiary" size="small">{t('区域')}</Text>
|
||||
<Input
|
||||
placeholder={t('区域')}
|
||||
value={region}
|
||||
onChange={(newValue) => updateValue(modelName, newValue)}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
theme="borderless"
|
||||
size="small"
|
||||
onClick={() => removeKeyValue(modelName)}
|
||||
className="hover:bg-red-50"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
<div className="flex justify-center pt-1">
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={addKeyValue}
|
||||
size="small"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
className="shadow-sm hover:shadow-md transition-shadow px-4"
|
||||
>
|
||||
{t('添加模型区域')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染可视化编辑器
|
||||
const renderVisualEditor = () => {
|
||||
switch (editorType) {
|
||||
case 'region':
|
||||
return renderRegionEditor();
|
||||
case 'object':
|
||||
return renderObjectEditor();
|
||||
case 'keyValue':
|
||||
default:
|
||||
return renderKeyValueEditor();
|
||||
}
|
||||
};
|
||||
|
||||
const hasJsonError = jsonError && jsonError.trim() !== '';
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{/* Label统一显示在上方 */}
|
||||
{label && (
|
||||
<div className="flex items-center">
|
||||
<Text className="text-sm font-medium text-gray-900">{label}</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 编辑模式切换 */}
|
||||
<div className="flex items-center justify-between p-2 bg-gray-50 rounded-md">
|
||||
<div className="flex items-center gap-2">
|
||||
{editMode === 'visual' && (
|
||||
<Text type="tertiary" size="small" className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded text-xs">
|
||||
{t('可视化模式')}
|
||||
</Text>
|
||||
)}
|
||||
{editMode === 'manual' && (
|
||||
<Text type="tertiary" size="small" className="bg-green-100 text-green-700 px-2 py-0.5 rounded text-xs">
|
||||
{t('手动编辑模式')}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{template && templateLabel && (
|
||||
<Button
|
||||
size="small"
|
||||
type="tertiary"
|
||||
onClick={fillTemplate}
|
||||
className="!text-semi-color-primary hover:bg-blue-50 text-xs"
|
||||
>
|
||||
{templateLabel}
|
||||
</Button>
|
||||
)}
|
||||
<Space size="tight">
|
||||
<Button
|
||||
size="small"
|
||||
type={editMode === 'visual' ? 'primary' : 'tertiary'}
|
||||
icon={<IconEdit />}
|
||||
onClick={toggleEditMode}
|
||||
disabled={editMode === 'manual' && hasJsonError}
|
||||
className={editMode === 'visual' ? 'shadow-sm' : ''}
|
||||
>
|
||||
{t('可视化')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type={editMode === 'manual' ? 'primary' : 'tertiary'}
|
||||
icon={<IconCode />}
|
||||
onClick={toggleEditMode}
|
||||
className={editMode === 'manual' ? 'shadow-sm' : ''}
|
||||
>
|
||||
{t('手动编辑')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* JSON错误提示 */}
|
||||
{hasJsonError && (
|
||||
<Banner
|
||||
type="danger"
|
||||
description={`JSON 格式错误: ${jsonError}`}
|
||||
className="!rounded-md text-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 编辑器内容 */}
|
||||
{editMode === 'visual' ? (
|
||||
<div>
|
||||
<Card className="!p-3 !border-gray-200 !shadow-sm !rounded-md bg-white">
|
||||
{renderVisualEditor()}
|
||||
</Card>
|
||||
{/* 可视化模式下的额外文本显示在下方 */}
|
||||
{extraText && (
|
||||
<div className="text-xs text-gray-600 mt-0.5">
|
||||
{extraText}
|
||||
</div>
|
||||
)}
|
||||
{/* 隐藏的Form字段用于验证和数据绑定 */}
|
||||
<Form.Input
|
||||
field={field}
|
||||
value={value}
|
||||
rules={rules}
|
||||
style={{ display: 'none' }}
|
||||
noLabel={true}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Form.TextArea
|
||||
field={field}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={handleManualChange}
|
||||
showClear={showClear}
|
||||
rows={Math.max(8, value ? value.split('\n').length : 8)}
|
||||
rules={rules}
|
||||
noLabel={true}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 额外文本在手动编辑模式下显示 */}
|
||||
{extraText && editMode === 'manual' && (
|
||||
<div className="text-xs text-gray-600">
|
||||
{extraText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JSONEditor;
|
||||
@@ -17,8 +17,6 @@ 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, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
@@ -26,37 +24,42 @@ import {
|
||||
showSuccess,
|
||||
verifyJSON,
|
||||
} from '../../../../helpers';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile.js';
|
||||
import { CHANNEL_OPTIONS } from '../../../../constants';
|
||||
import {
|
||||
Avatar,
|
||||
Banner,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Col,
|
||||
Form,
|
||||
Highlight,
|
||||
ImagePreview,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
SideSheet,
|
||||
Space,
|
||||
Spin,
|
||||
Button,
|
||||
Typography,
|
||||
Checkbox,
|
||||
Banner,
|
||||
Modal,
|
||||
ImagePreview,
|
||||
Card,
|
||||
Tag,
|
||||
Avatar,
|
||||
Form,
|
||||
Row,
|
||||
Col,
|
||||
Highlight,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { getChannelModels, copy, getChannelIcon, getModelCategories, selectFilter } from '../../../../helpers';
|
||||
import ModelSelectModal from './ModelSelectModal';
|
||||
import JSONEditor from '../../../common/JSONEditor';
|
||||
import { CHANNEL_OPTIONS, CLAUDE_CODE_DEFAULT_SYSTEM_PROMPT } from '../../../../constants';
|
||||
import {
|
||||
IconSave,
|
||||
IconBolt,
|
||||
IconClose,
|
||||
IconServer,
|
||||
IconSetting,
|
||||
IconCode,
|
||||
IconGlobe,
|
||||
IconBolt,
|
||||
IconSave,
|
||||
IconServer,
|
||||
IconSetting,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
@@ -69,7 +72,9 @@ const STATUS_CODE_MAPPING_EXAMPLE = {
|
||||
};
|
||||
|
||||
const REGION_EXAMPLE = {
|
||||
default: 'us-central1',
|
||||
"default": 'global',
|
||||
"gemini-1.5-pro-002": "europe-west2",
|
||||
"gemini-1.5-flash-002": "europe-west2",
|
||||
'claude-3-5-sonnet-20240620': 'europe-west1',
|
||||
};
|
||||
|
||||
@@ -90,6 +95,8 @@ function type2secretPrompt(type) {
|
||||
return '按照如下格式输入: AccessKey|SecretKey, 如果上游是New API,则直接输ApiKey';
|
||||
case 51:
|
||||
return '按照如下格式输入: Access Key ID|Secret Access Key';
|
||||
case 53:
|
||||
return '按照如下格式输入:AccessToken|RefreshToken';
|
||||
default:
|
||||
return '请输入渠道对应的鉴权密钥';
|
||||
}
|
||||
@@ -142,6 +149,10 @@ const EditChannelModal = (props) => {
|
||||
const [customModel, setCustomModel] = useState('');
|
||||
const [modalImageUrl, setModalImageUrl] = useState('');
|
||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||||
const [showOAuthModal, setShowOAuthModal] = useState(false);
|
||||
const [authorizationCode, setAuthorizationCode] = useState('');
|
||||
const [oauthParams, setOauthParams] = useState(null);
|
||||
const [isExchangingCode, setIsExchangingCode] = useState(false);
|
||||
const [modelModalVisible, setModelModalVisible] = useState(false);
|
||||
const [fetchedModels, setFetchedModels] = useState([]);
|
||||
const formApiRef = useRef(null);
|
||||
@@ -350,6 +361,24 @@ const EditChannelModal = (props) => {
|
||||
data.system_prompt = '';
|
||||
}
|
||||
|
||||
// 特殊处理Claude Code渠道的密钥拆分和系统提示词
|
||||
if (data.type === 53) {
|
||||
// 拆分密钥
|
||||
if (data.key) {
|
||||
const keyParts = data.key.split('|');
|
||||
if (keyParts.length === 2) {
|
||||
data.access_token = keyParts[0];
|
||||
data.refresh_token = keyParts[1];
|
||||
} else {
|
||||
// 如果没有 | 分隔符,表示只有access token
|
||||
data.access_token = data.key;
|
||||
data.refresh_token = '';
|
||||
}
|
||||
}
|
||||
// 强制设置固定系统提示词
|
||||
data.system_prompt = CLAUDE_CODE_DEFAULT_SYSTEM_PROMPT;
|
||||
}
|
||||
|
||||
setInputs(data);
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValues(data);
|
||||
@@ -473,6 +502,72 @@ const EditChannelModal = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 生成OAuth授权URL
|
||||
const handleGenerateOAuth = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await API.get('/api/channel/claude/oauth/url');
|
||||
if (res.data.success) {
|
||||
setOauthParams(res.data.data);
|
||||
setShowOAuthModal(true);
|
||||
showSuccess(t('OAuth授权URL生成成功'));
|
||||
} else {
|
||||
showError(res.data.message || t('生成OAuth授权URL失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('生成OAuth授权URL失败:') + error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 交换授权码
|
||||
const handleExchangeCode = async () => {
|
||||
if (!authorizationCode.trim()) {
|
||||
showError(t('请输入授权码'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!oauthParams) {
|
||||
showError(t('OAuth参数丢失,请重新生成'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsExchangingCode(true);
|
||||
const res = await API.post('/api/channel/claude/oauth/exchange', {
|
||||
authorization_code: authorizationCode,
|
||||
code_verifier: oauthParams.code_verifier,
|
||||
state: oauthParams.state,
|
||||
});
|
||||
|
||||
if (res.data.success) {
|
||||
const tokenData = res.data.data;
|
||||
// 自动填充access token和refresh token
|
||||
handleInputChange('access_token', tokenData.access_token);
|
||||
handleInputChange('refresh_token', tokenData.refresh_token);
|
||||
handleInputChange('key', `${tokenData.access_token}|${tokenData.refresh_token}`);
|
||||
|
||||
// 更新表单字段
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('access_token', tokenData.access_token);
|
||||
formApiRef.current.setValue('refresh_token', tokenData.refresh_token);
|
||||
}
|
||||
|
||||
setShowOAuthModal(false);
|
||||
setAuthorizationCode('');
|
||||
setOauthParams(null);
|
||||
showSuccess(t('授权码交换成功,已自动填充tokens'));
|
||||
} else {
|
||||
showError(res.data.message || t('授权码交换失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('授权码交换失败:') + error.message);
|
||||
} finally {
|
||||
setIsExchangingCode(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const modelMap = new Map();
|
||||
|
||||
@@ -785,7 +880,7 @@ const EditChannelModal = (props) => {
|
||||
const batchExtra = batchAllowed ? (
|
||||
<Space>
|
||||
<Checkbox
|
||||
disabled={isEdit}
|
||||
disabled={isEdit || inputs.type === 53}
|
||||
checked={batch}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
@@ -1121,6 +1216,49 @@ const EditChannelModal = (props) => {
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : inputs.type === 53 ? (
|
||||
<>
|
||||
<Form.Input
|
||||
field='access_token'
|
||||
label={isEdit ? t('Access Token(编辑模式下,保存的密钥不会显示)') : t('Access Token')}
|
||||
placeholder={t('sk-ant-xxx')}
|
||||
rules={isEdit ? [] : [{ required: true, message: t('请输入Access Token') }]}
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => {
|
||||
handleInputChange('access_token', value);
|
||||
// 同时更新key字段,格式为access_token|refresh_token
|
||||
const refreshToken = inputs.refresh_token || '';
|
||||
handleInputChange('key', `${value}|${refreshToken}`);
|
||||
}}
|
||||
suffix={
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
theme="light"
|
||||
onClick={handleGenerateOAuth}
|
||||
>
|
||||
{t('生成OAuth授权码')}
|
||||
</Button>
|
||||
}
|
||||
extraText={batchExtra}
|
||||
showClear
|
||||
/>
|
||||
<Form.Input
|
||||
field='refresh_token'
|
||||
label={isEdit ? t('Refresh Token(编辑模式下,保存的密钥不会显示)') : t('Refresh Token')}
|
||||
placeholder={t('sk-ant-xxx(可选)')}
|
||||
rules={[]}
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => {
|
||||
handleInputChange('refresh_token', value);
|
||||
// 同时更新key字段,格式为access_token|refresh_token
|
||||
const accessToken = inputs.access_token || '';
|
||||
handleInputChange('key', `${accessToken}|${value}`);
|
||||
}}
|
||||
extraText={batchExtra}
|
||||
showClear
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Form.Input
|
||||
field='key'
|
||||
@@ -1174,24 +1312,24 @@ const EditChannelModal = (props) => {
|
||||
)}
|
||||
|
||||
{inputs.type === 41 && (
|
||||
<Form.TextArea
|
||||
<JSONEditor
|
||||
field='other'
|
||||
label={t('部署地区')}
|
||||
placeholder={t(
|
||||
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n "default": "us-central1",\n "claude-3-5-sonnet-20240620": "europe-west1"\n}'
|
||||
)}
|
||||
autosize
|
||||
value={inputs.other || ''}
|
||||
onChange={(value) => handleInputChange('other', value)}
|
||||
rules={[{ required: true, message: t('请填写部署地区') }]}
|
||||
template={REGION_EXAMPLE}
|
||||
templateLabel={t('填入模板')}
|
||||
editorType="region"
|
||||
formApi={formApiRef.current}
|
||||
extraText={
|
||||
<Text
|
||||
className="!text-semi-color-primary cursor-pointer"
|
||||
onClick={() => handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
|
||||
>
|
||||
{t('填入模板')}
|
||||
<Text type="tertiary" size="small">
|
||||
{t('设置默认地区和特定模型的专用地区')}
|
||||
</Text>
|
||||
}
|
||||
showClear
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1447,24 +1585,24 @@ const EditChannelModal = (props) => {
|
||||
showClear
|
||||
/>
|
||||
|
||||
<Form.TextArea
|
||||
<JSONEditor
|
||||
field='model_mapping'
|
||||
label={t('模型重定向')}
|
||||
placeholder={
|
||||
t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') +
|
||||
`\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`
|
||||
}
|
||||
autosize
|
||||
value={inputs.model_mapping || ''}
|
||||
onChange={(value) => handleInputChange('model_mapping', value)}
|
||||
template={MODEL_MAPPING_EXAMPLE}
|
||||
templateLabel={t('填入模板')}
|
||||
editorType="keyValue"
|
||||
formApi={formApiRef.current}
|
||||
extraText={
|
||||
<Text
|
||||
className="!text-semi-color-primary cursor-pointer"
|
||||
onClick={() => handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))}
|
||||
>
|
||||
{t('填入模板')}
|
||||
<Text type="tertiary" size="small">
|
||||
{t('键为请求中的模型名称,值为要替换的模型名称')}
|
||||
</Text>
|
||||
}
|
||||
showClear
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -1554,7 +1692,7 @@ const EditChannelModal = (props) => {
|
||||
showClear
|
||||
/>
|
||||
|
||||
<Form.TextArea
|
||||
<JSONEditor
|
||||
field='status_code_mapping'
|
||||
label={t('状态码复写')}
|
||||
placeholder={
|
||||
@@ -1562,17 +1700,17 @@ const EditChannelModal = (props) => {
|
||||
'\n' +
|
||||
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
|
||||
}
|
||||
autosize
|
||||
value={inputs.status_code_mapping || ''}
|
||||
onChange={(value) => handleInputChange('status_code_mapping', value)}
|
||||
template={STATUS_CODE_MAPPING_EXAMPLE}
|
||||
templateLabel={t('填入模板')}
|
||||
editorType="keyValue"
|
||||
formApi={formApiRef.current}
|
||||
extraText={
|
||||
<Text
|
||||
className="!text-semi-color-primary cursor-pointer"
|
||||
onClick={() => handleInputChange('status_code_mapping', JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2))}
|
||||
>
|
||||
{t('填入模板')}
|
||||
<Text type="tertiary" size="small">
|
||||
{t('键为原状态码,值为要复写的状态码,仅影响本地判断')}
|
||||
</Text>
|
||||
}
|
||||
showClear
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -1585,14 +1723,6 @@ const EditChannelModal = (props) => {
|
||||
</Avatar>
|
||||
<div>
|
||||
<Text className="text-lg font-medium">{t('渠道额外设置')}</Text>
|
||||
<div className="text-xs text-gray-600">
|
||||
<Text
|
||||
className="!text-semi-color-primary cursor-pointer"
|
||||
onClick={() => window.open('https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md')}
|
||||
>
|
||||
{t('设置说明')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1637,11 +1767,19 @@ const EditChannelModal = (props) => {
|
||||
<Form.TextArea
|
||||
field='system_prompt'
|
||||
label={t('系统提示词')}
|
||||
placeholder={t('输入系统提示词,用户的系统提示词将优先于此设置')}
|
||||
onChange={(value) => handleChannelSettingsChange('system_prompt', value)}
|
||||
placeholder={inputs.type === 53 ? CLAUDE_CODE_DEFAULT_SYSTEM_PROMPT : t('输入系统提示词,用户的系统提示词将优先于此设置')}
|
||||
onChange={(value) => {
|
||||
if (inputs.type === 53) {
|
||||
// Claude Code渠道系统提示词固定,不允许修改
|
||||
return;
|
||||
}
|
||||
handleChannelSettingsChange('system_prompt', value);
|
||||
}}
|
||||
disabled={inputs.type === 53}
|
||||
value={inputs.type === 53 ? CLAUDE_CODE_DEFAULT_SYSTEM_PROMPT : undefined}
|
||||
autosize
|
||||
showClear
|
||||
extraText={t('用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置')}
|
||||
showClear={inputs.type !== 53}
|
||||
extraText={inputs.type === 53 ? t('Claude Code渠道系统提示词固定为官方CLI身份,不可修改') : t('用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置')}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -1665,8 +1803,70 @@ const EditChannelModal = (props) => {
|
||||
}}
|
||||
onCancel={() => setModelModalVisible(false)}
|
||||
/>
|
||||
|
||||
{/* OAuth Authorization Modal */}
|
||||
<Modal
|
||||
title={t('生成Claude Code OAuth授权码')}
|
||||
visible={showOAuthModal}
|
||||
onCancel={() => {
|
||||
setShowOAuthModal(false);
|
||||
setAuthorizationCode('');
|
||||
setOauthParams(null);
|
||||
}}
|
||||
onOk={handleExchangeCode}
|
||||
okText={isExchangingCode ? t('交换中...') : t('确认')}
|
||||
cancelText={t('取消')}
|
||||
confirmLoading={isExchangingCode}
|
||||
width={600}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2 block">{t('请访问以下授权地址:')}</Text>
|
||||
<div className="p-3 bg-gray-50 rounded-lg border">
|
||||
<Text
|
||||
link
|
||||
underline
|
||||
className="text-sm font-mono break-all cursor-pointer text-blue-600 hover:text-blue-800"
|
||||
onClick={() => {
|
||||
if (oauthParams?.auth_url) {
|
||||
window.open(oauthParams.auth_url, '_blank');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{oauthParams?.auth_url || t('正在生成授权地址...')}
|
||||
</Text>
|
||||
<div className="mt-2">
|
||||
<Text
|
||||
copyable={{ content: oauthParams?.auth_url }}
|
||||
type="tertiary"
|
||||
size="small"
|
||||
>
|
||||
{t('复制链接')}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-2 block">{t('授权后,请将获得的授权码粘贴到下方:')}</Text>
|
||||
<Input
|
||||
value={authorizationCode}
|
||||
onChange={setAuthorizationCode}
|
||||
placeholder={t('请输入授权码')}
|
||||
showClear
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Banner
|
||||
type="info"
|
||||
description={t('获得授权码后,系统将自动换取access token和refresh token并填充到表单中。')}
|
||||
className="!rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditChannelModal;
|
||||
export default EditChannelModal;
|
||||
Reference in New Issue
Block a user