EditChannel.js 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192
  1. import React, { useEffect, useState } from 'react';
  2. import { useNavigate } from 'react-router-dom';
  3. import { useTranslation } from 'react-i18next';
  4. import {
  5. API,
  6. isMobile,
  7. showError,
  8. showInfo,
  9. showSuccess,
  10. verifyJSON,
  11. } from '../../helpers';
  12. import { CHANNEL_OPTIONS } from '../../constants';
  13. import {
  14. SideSheet,
  15. Space,
  16. Spin,
  17. Button,
  18. Input,
  19. Typography,
  20. Select,
  21. TextArea,
  22. Checkbox,
  23. Banner,
  24. Modal,
  25. ImagePreview,
  26. Card,
  27. Tag,
  28. } from '@douyinfe/semi-ui';
  29. import { getChannelModels } from '../../helpers';
  30. import {
  31. IconSave,
  32. IconClose,
  33. IconServer,
  34. IconSetting,
  35. IconCode,
  36. IconGlobe,
  37. } from '@douyinfe/semi-icons';
  38. const { Text, Title } = Typography;
  39. const MODEL_MAPPING_EXAMPLE = {
  40. 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
  41. };
  42. const STATUS_CODE_MAPPING_EXAMPLE = {
  43. 400: '500',
  44. };
  45. const REGION_EXAMPLE = {
  46. default: 'us-central1',
  47. 'claude-3-5-sonnet-20240620': 'europe-west1',
  48. };
  49. function type2secretPrompt(type) {
  50. // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
  51. switch (type) {
  52. case 15:
  53. return '按照如下格式输入:APIKey|SecretKey';
  54. case 18:
  55. return '按照如下格式输入:APPID|APISecret|APIKey';
  56. case 22:
  57. return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
  58. case 23:
  59. return '按照如下格式输入:AppId|SecretId|SecretKey';
  60. case 33:
  61. return '按照如下格式输入:Ak|Sk|Region';
  62. case 50:
  63. return '按照如下格式输入: AccessKey|SecretKey';
  64. default:
  65. return '请输入渠道对应的鉴权密钥';
  66. }
  67. }
  68. const EditChannel = (props) => {
  69. const { t } = useTranslation();
  70. const navigate = useNavigate();
  71. const channelId = props.editingChannel.id;
  72. const isEdit = channelId !== undefined;
  73. const [loading, setLoading] = useState(isEdit);
  74. const handleCancel = () => {
  75. props.handleClose();
  76. };
  77. const originInputs = {
  78. name: '',
  79. type: 1,
  80. key: '',
  81. openai_organization: '',
  82. max_input_tokens: 0,
  83. base_url: '',
  84. other: '',
  85. model_mapping: '',
  86. status_code_mapping: '',
  87. models: [],
  88. auto_ban: 1,
  89. test_model: '',
  90. groups: ['default'],
  91. priority: 0,
  92. weight: 0,
  93. tag: '',
  94. };
  95. const [batch, setBatch] = useState(false);
  96. const [autoBan, setAutoBan] = useState(true);
  97. // const [autoBan, setAutoBan] = useState(true);
  98. const [inputs, setInputs] = useState(originInputs);
  99. const [originModelOptions, setOriginModelOptions] = useState([]);
  100. const [modelOptions, setModelOptions] = useState([]);
  101. const [groupOptions, setGroupOptions] = useState([]);
  102. const [basicModels, setBasicModels] = useState([]);
  103. const [fullModels, setFullModels] = useState([]);
  104. const [customModel, setCustomModel] = useState('');
  105. const [modalImageUrl, setModalImageUrl] = useState('');
  106. const [isModalOpenurl, setIsModalOpenurl] = useState(false);
  107. const handleInputChange = (name, value) => {
  108. if (name === 'base_url' && value.endsWith('/v1')) {
  109. Modal.confirm({
  110. title: '警告',
  111. content:
  112. '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
  113. onOk: () => {
  114. setInputs((inputs) => ({ ...inputs, [name]: value }));
  115. },
  116. });
  117. return;
  118. }
  119. setInputs((inputs) => ({ ...inputs, [name]: value }));
  120. if (name === 'type') {
  121. let localModels = [];
  122. switch (value) {
  123. case 2:
  124. localModels = [
  125. 'mj_imagine',
  126. 'mj_variation',
  127. 'mj_reroll',
  128. 'mj_blend',
  129. 'mj_upscale',
  130. 'mj_describe',
  131. 'mj_uploads',
  132. ];
  133. break;
  134. case 5:
  135. localModels = [
  136. 'swap_face',
  137. 'mj_imagine',
  138. 'mj_variation',
  139. 'mj_reroll',
  140. 'mj_blend',
  141. 'mj_upscale',
  142. 'mj_describe',
  143. 'mj_zoom',
  144. 'mj_shorten',
  145. 'mj_modal',
  146. 'mj_inpaint',
  147. 'mj_custom_zoom',
  148. 'mj_high_variation',
  149. 'mj_low_variation',
  150. 'mj_pan',
  151. 'mj_uploads',
  152. ];
  153. break;
  154. case 36:
  155. localModels = ['suno_music', 'suno_lyrics'];
  156. break;
  157. default:
  158. localModels = getChannelModels(value);
  159. break;
  160. }
  161. if (inputs.models.length === 0) {
  162. setInputs((inputs) => ({ ...inputs, models: localModels }));
  163. }
  164. setBasicModels(localModels);
  165. }
  166. //setAutoBan
  167. };
  168. const loadChannel = async () => {
  169. setLoading(true);
  170. let res = await API.get(`/api/channel/${channelId}`);
  171. if (res === undefined) {
  172. return;
  173. }
  174. const { success, message, data } = res.data;
  175. if (success) {
  176. if (data.models === '') {
  177. data.models = [];
  178. } else {
  179. data.models = data.models.split(',');
  180. }
  181. if (data.group === '') {
  182. data.groups = [];
  183. } else {
  184. data.groups = data.group.split(',');
  185. }
  186. if (data.model_mapping !== '') {
  187. data.model_mapping = JSON.stringify(
  188. JSON.parse(data.model_mapping),
  189. null,
  190. 2,
  191. );
  192. }
  193. setInputs(data);
  194. if (data.auto_ban === 0) {
  195. setAutoBan(false);
  196. } else {
  197. setAutoBan(true);
  198. }
  199. setBasicModels(getChannelModels(data.type));
  200. // console.log(data);
  201. } else {
  202. showError(message);
  203. }
  204. setLoading(false);
  205. };
  206. const fetchUpstreamModelList = async (name) => {
  207. // if (inputs['type'] !== 1) {
  208. // showError(t('仅支持 OpenAI 接口格式'));
  209. // return;
  210. // }
  211. setLoading(true);
  212. const models = inputs['models'] || [];
  213. let err = false;
  214. if (isEdit) {
  215. // 如果是编辑模式,使用已有的channel id获取模型列表
  216. const res = await API.get('/api/channel/fetch_models/' + channelId);
  217. if (res.data && res.data?.success) {
  218. models.push(...res.data.data);
  219. } else {
  220. err = true;
  221. }
  222. } else {
  223. // 如果是新建模式,通过后端代理获取模型列表
  224. if (!inputs?.['key']) {
  225. showError(t('请填写密钥'));
  226. err = true;
  227. } else {
  228. try {
  229. const res = await API.post('/api/channel/fetch_models', {
  230. base_url: inputs['base_url'],
  231. type: inputs['type'],
  232. key: inputs['key'],
  233. });
  234. if (res.data && res.data.success) {
  235. models.push(...res.data.data);
  236. } else {
  237. err = true;
  238. }
  239. } catch (error) {
  240. console.error('Error fetching models:', error);
  241. err = true;
  242. }
  243. }
  244. }
  245. if (!err) {
  246. handleInputChange(name, Array.from(new Set(models)));
  247. showSuccess(t('获取模型列表成功'));
  248. } else {
  249. showError(t('获取模型列表失败'));
  250. }
  251. setLoading(false);
  252. };
  253. const fetchModels = async () => {
  254. try {
  255. let res = await API.get(`/api/channel/models`);
  256. let localModelOptions = res.data.data.map((model) => ({
  257. label: model.id,
  258. value: model.id,
  259. }));
  260. setOriginModelOptions(localModelOptions);
  261. setFullModels(res.data.data.map((model) => model.id));
  262. setBasicModels(
  263. res.data.data
  264. .filter((model) => {
  265. return model.id.startsWith('gpt-') || model.id.startsWith('text-');
  266. })
  267. .map((model) => model.id),
  268. );
  269. } catch (error) {
  270. showError(error.message);
  271. }
  272. };
  273. const fetchGroups = async () => {
  274. try {
  275. let res = await API.get(`/api/group/`);
  276. if (res === undefined) {
  277. return;
  278. }
  279. setGroupOptions(
  280. res.data.data.map((group) => ({
  281. label: group,
  282. value: group,
  283. })),
  284. );
  285. } catch (error) {
  286. showError(error.message);
  287. }
  288. };
  289. useEffect(() => {
  290. // 使用 Map 来避免重复,以 value 为键
  291. const modelMap = new Map();
  292. // 先添加原始模型选项
  293. originModelOptions.forEach(option => {
  294. modelMap.set(option.value, option);
  295. });
  296. // 再添加当前选中的模型(如果不存在)
  297. inputs.models.forEach(model => {
  298. if (!modelMap.has(model)) {
  299. modelMap.set(model, {
  300. label: model,
  301. value: model,
  302. });
  303. }
  304. });
  305. setModelOptions(Array.from(modelMap.values()));
  306. }, [originModelOptions, inputs.models]);
  307. useEffect(() => {
  308. fetchModels().then();
  309. fetchGroups().then();
  310. if (isEdit) {
  311. loadChannel().then(() => { });
  312. } else {
  313. setInputs(originInputs);
  314. let localModels = getChannelModels(inputs.type);
  315. setBasicModels(localModels);
  316. setInputs((inputs) => ({ ...inputs, models: localModels }));
  317. }
  318. }, [props.editingChannel.id]);
  319. const submit = async () => {
  320. if (!isEdit && (inputs.name === '' || inputs.key === '')) {
  321. showInfo(t('请填写渠道名称和渠道密钥!'));
  322. return;
  323. }
  324. if (inputs.models.length === 0) {
  325. showInfo(t('请至少选择一个模型!'));
  326. return;
  327. }
  328. if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
  329. showInfo(t('模型映射必须是合法的 JSON 格式!'));
  330. return;
  331. }
  332. let localInputs = { ...inputs };
  333. if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
  334. localInputs.base_url = localInputs.base_url.slice(
  335. 0,
  336. localInputs.base_url.length - 1,
  337. );
  338. }
  339. if (localInputs.type === 18 && localInputs.other === '') {
  340. localInputs.other = 'v2.1';
  341. }
  342. let res;
  343. if (!Array.isArray(localInputs.models)) {
  344. showError(t('提交失败,请勿重复提交!'));
  345. handleCancel();
  346. return;
  347. }
  348. localInputs.auto_ban = autoBan ? 1 : 0;
  349. localInputs.models = localInputs.models.join(',');
  350. localInputs.group = localInputs.groups.join(',');
  351. if (isEdit) {
  352. res = await API.put(`/api/channel/`, {
  353. ...localInputs,
  354. id: parseInt(channelId),
  355. });
  356. } else {
  357. res = await API.post(`/api/channel/`, localInputs);
  358. }
  359. const { success, message } = res.data;
  360. if (success) {
  361. if (isEdit) {
  362. showSuccess(t('渠道更新成功!'));
  363. } else {
  364. showSuccess(t('渠道创建成功!'));
  365. setInputs(originInputs);
  366. }
  367. props.refresh();
  368. props.handleClose();
  369. } else {
  370. showError(message);
  371. }
  372. };
  373. const addCustomModels = () => {
  374. if (customModel.trim() === '') return;
  375. const modelArray = customModel.split(',').map((model) => model.trim());
  376. let localModels = [...inputs.models];
  377. let localModelOptions = [...modelOptions];
  378. const addedModels = [];
  379. modelArray.forEach((model) => {
  380. if (model && !localModels.includes(model)) {
  381. localModels.push(model);
  382. localModelOptions.push({
  383. key: model,
  384. text: model,
  385. value: model,
  386. });
  387. addedModels.push(model);
  388. }
  389. });
  390. setModelOptions(localModelOptions);
  391. setCustomModel('');
  392. handleInputChange('models', localModels);
  393. if (addedModels.length > 0) {
  394. showSuccess(
  395. t('已新增 {{count}} 个模型:{{list}}', {
  396. count: addedModels.length,
  397. list: addedModels.join(', '),
  398. })
  399. );
  400. } else {
  401. showInfo(t('未发现新增模型'));
  402. }
  403. };
  404. return (
  405. <>
  406. <SideSheet
  407. placement={isEdit ? 'right' : 'left'}
  408. title={
  409. <Space>
  410. <Tag color="blue" shape="circle">{isEdit ? t('编辑') : t('新建')}</Tag>
  411. <Title heading={4} className="m-0">
  412. {isEdit ? t('更新渠道信息') : t('创建新的渠道')}
  413. </Title>
  414. </Space>
  415. }
  416. headerStyle={{
  417. borderBottom: '1px solid var(--semi-color-border)',
  418. padding: '24px'
  419. }}
  420. bodyStyle={{
  421. backgroundColor: 'var(--semi-color-bg-0)',
  422. padding: '0'
  423. }}
  424. visible={props.visible}
  425. width={isMobile() ? '100%' : 600}
  426. footer={
  427. <div className="flex justify-end bg-white">
  428. <Space>
  429. <Button
  430. theme="solid"
  431. size="large"
  432. className="!rounded-full"
  433. onClick={submit}
  434. icon={<IconSave />}
  435. >
  436. {t('提交')}
  437. </Button>
  438. <Button
  439. theme="light"
  440. size="large"
  441. className="!rounded-full"
  442. type="primary"
  443. onClick={handleCancel}
  444. icon={<IconClose />}
  445. >
  446. {t('取消')}
  447. </Button>
  448. </Space>
  449. </div>
  450. }
  451. closeIcon={null}
  452. onCancel={() => handleCancel()}
  453. >
  454. <Spin spinning={loading}>
  455. <div className="p-6">
  456. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  457. <div className="flex items-center mb-4 p-6 rounded-xl" style={{
  458. background: 'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)',
  459. position: 'relative'
  460. }}>
  461. <div className="absolute inset-0 overflow-hidden">
  462. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
  463. <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
  464. </div>
  465. <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
  466. <IconServer size="large" style={{ color: '#ffffff' }} />
  467. </div>
  468. <div className="relative">
  469. <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('基本信息')}</Text>
  470. <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('渠道的基本配置信息')}</div>
  471. </div>
  472. </div>
  473. <div className="space-y-4">
  474. <div>
  475. <Text strong className="block mb-2">{t('类型')}</Text>
  476. <Select
  477. name='type'
  478. required
  479. optionList={CHANNEL_OPTIONS}
  480. value={inputs.type}
  481. onChange={(value) => handleInputChange('type', value)}
  482. style={{ width: '100%' }}
  483. filter
  484. searchPosition='dropdown'
  485. placeholder={t('请选择渠道类型')}
  486. size="large"
  487. className="!rounded-lg"
  488. />
  489. </div>
  490. <div>
  491. <Text strong className="block mb-2">{t('名称')}</Text>
  492. <Input
  493. required
  494. name='name'
  495. placeholder={t('请为渠道命名')}
  496. onChange={(value) => {
  497. handleInputChange('name', value);
  498. }}
  499. value={inputs.name}
  500. autoComplete='new-password'
  501. size="large"
  502. className="!rounded-lg"
  503. />
  504. </div>
  505. <div>
  506. <Text strong className="block mb-2">{t('密钥')}</Text>
  507. {batch ? (
  508. <TextArea
  509. name='key'
  510. required
  511. placeholder={t('请输入密钥,一行一个')}
  512. onChange={(value) => {
  513. handleInputChange('key', value);
  514. }}
  515. value={inputs.key}
  516. autosize={{ minRows: 6, maxRows: 6 }}
  517. autoComplete='new-password'
  518. className="!rounded-lg"
  519. />
  520. ) : (
  521. <>
  522. {inputs.type === 41 ? (
  523. <TextArea
  524. name='key'
  525. required
  526. placeholder={
  527. '{\n' +
  528. ' "type": "service_account",\n' +
  529. ' "project_id": "abc-bcd-123-456",\n' +
  530. ' "private_key_id": "123xxxxx456",\n' +
  531. ' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' +
  532. ' "client_email": "xxx@developer.gserviceaccount.com",\n' +
  533. ' "client_id": "111222333",\n' +
  534. ' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' +
  535. ' "token_uri": "https://oauth2.googleapis.com/token",\n' +
  536. ' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
  537. ' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
  538. ' "universe_domain": "googleapis.com"\n' +
  539. '}'
  540. }
  541. onChange={(value) => {
  542. handleInputChange('key', value);
  543. }}
  544. autosize={{ minRows: 10 }}
  545. value={inputs.key}
  546. autoComplete='new-password'
  547. className="!rounded-lg font-mono"
  548. />
  549. ) : (
  550. <Input
  551. name='key'
  552. required
  553. placeholder={t(type2secretPrompt(inputs.type))}
  554. onChange={(value) => {
  555. handleInputChange('key', value);
  556. }}
  557. value={inputs.key}
  558. autoComplete='new-password'
  559. size="large"
  560. className="!rounded-lg"
  561. />
  562. )}
  563. </>
  564. )}
  565. </div>
  566. {!isEdit && (
  567. <div className="flex items-center">
  568. <Checkbox
  569. checked={batch}
  570. onChange={() => setBatch(!batch)}
  571. />
  572. <Text strong className="ml-2">{t('批量创建')}</Text>
  573. </div>
  574. )}
  575. </div>
  576. </Card>
  577. {/* API Configuration Card */}
  578. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  579. <div className="flex items-center mb-4 p-6 rounded-xl" style={{
  580. background: 'linear-gradient(135deg, #065f46 0%, #059669 50%, #10b981 100%)',
  581. position: 'relative'
  582. }}>
  583. <div className="absolute inset-0 overflow-hidden">
  584. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
  585. <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
  586. </div>
  587. <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
  588. <IconGlobe size="large" style={{ color: '#ffffff' }} />
  589. </div>
  590. <div className="relative">
  591. <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('API 配置')}</Text>
  592. <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('API 地址和相关配置')}</div>
  593. </div>
  594. </div>
  595. <div className="space-y-4">
  596. {inputs.type === 40 && (
  597. <Banner
  598. type='info'
  599. description={
  600. <div>
  601. <Text strong>{t('邀请链接')}:</Text>
  602. <Text
  603. link
  604. underline
  605. className="ml-2 cursor-pointer"
  606. onClick={() => window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')}
  607. >
  608. https://cloud.siliconflow.cn/i/hij0YNTZ
  609. </Text>
  610. </div>
  611. }
  612. className='!rounded-lg'
  613. />
  614. )}
  615. {inputs.type === 3 && (
  616. <>
  617. <Banner
  618. type='warning'
  619. description={t('2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."')}
  620. className='!rounded-lg'
  621. />
  622. <div>
  623. <Text strong className="block mb-2">AZURE_OPENAI_ENDPOINT</Text>
  624. <Input
  625. name='azure_base_url'
  626. placeholder={t('请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com')}
  627. onChange={(value) => handleInputChange('base_url', value)}
  628. value={inputs.base_url}
  629. autoComplete='new-password'
  630. size="large"
  631. className="!rounded-lg"
  632. />
  633. </div>
  634. <div>
  635. <Text strong className="block mb-2">{t('默认 API 版本')}</Text>
  636. <Input
  637. name='azure_other'
  638. placeholder={t('请输入默认 API 版本,例如:2025-04-01-preview')}
  639. onChange={(value) => handleInputChange('other', value)}
  640. value={inputs.other}
  641. autoComplete='new-password'
  642. size="large"
  643. className="!rounded-lg"
  644. />
  645. </div>
  646. </>
  647. )}
  648. {inputs.type === 8 && (
  649. <>
  650. <Banner
  651. type='warning'
  652. description={t('如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。')}
  653. className='!rounded-lg'
  654. />
  655. <div>
  656. <Text strong className="block mb-2">{t('完整的 Base URL,支持变量{model}')}</Text>
  657. <Input
  658. name='base_url'
  659. placeholder={t('请输入完整的URL,例如:https://api.openai.com/v1/chat/completions')}
  660. onChange={(value) => handleInputChange('base_url', value)}
  661. value={inputs.base_url}
  662. autoComplete='new-password'
  663. size="large"
  664. className="!rounded-lg"
  665. />
  666. </div>
  667. </>
  668. )}
  669. {inputs.type === 37 && (
  670. <Banner
  671. type='warning'
  672. description={t('Dify渠道只适配chatflow和agent,并且agent不支持图片!')}
  673. className='!rounded-lg'
  674. />
  675. )}
  676. {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
  677. <div>
  678. <Text strong className="block mb-2">{t('API地址')}</Text>
  679. <Input
  680. name='base_url'
  681. placeholder={t('此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/')}
  682. onChange={(value) => handleInputChange('base_url', value)}
  683. value={inputs.base_url}
  684. autoComplete='new-password'
  685. size="large"
  686. className="!rounded-lg"
  687. />
  688. <Text type="tertiary" className="mt-1 text-xs">
  689. {t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')}
  690. </Text>
  691. </div>
  692. )}
  693. {inputs.type === 22 && (
  694. <div>
  695. <Text strong className="block mb-2">{t('私有部署地址')}</Text>
  696. <Input
  697. name='base_url'
  698. placeholder={t('请输入私有部署地址,格式为:https://fastgpt.run/api/openapi')}
  699. onChange={(value) => handleInputChange('base_url', value)}
  700. value={inputs.base_url}
  701. autoComplete='new-password'
  702. size="large"
  703. className="!rounded-lg"
  704. />
  705. </div>
  706. )}
  707. {inputs.type === 36 && (
  708. <div>
  709. <Text strong className="block mb-2">
  710. {t('注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用')}
  711. </Text>
  712. <Input
  713. name='base_url'
  714. placeholder={t('请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com')}
  715. onChange={(value) => handleInputChange('base_url', value)}
  716. value={inputs.base_url}
  717. autoComplete='new-password'
  718. size="large"
  719. className="!rounded-lg"
  720. />
  721. </div>
  722. )}
  723. </div>
  724. </Card>
  725. {/* Model Configuration Card */}
  726. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  727. <div className="flex items-center mb-4 p-6 rounded-xl" style={{
  728. background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
  729. position: 'relative'
  730. }}>
  731. <div className="absolute inset-0 overflow-hidden">
  732. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
  733. <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
  734. </div>
  735. <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
  736. <IconCode size="large" style={{ color: '#ffffff' }} />
  737. </div>
  738. <div className="relative">
  739. <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('模型配置')}</Text>
  740. <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('模型选择和映射设置')}</div>
  741. </div>
  742. </div>
  743. <div className="space-y-4">
  744. <div>
  745. <Text strong className="block mb-2">{t('模型')}</Text>
  746. <Select
  747. placeholder={t('请选择该渠道所支持的模型')}
  748. name='models'
  749. required
  750. multiple
  751. selection
  752. filter
  753. searchPosition='dropdown'
  754. onChange={(value) => handleInputChange('models', value)}
  755. value={inputs.models}
  756. autoComplete='new-password'
  757. optionList={modelOptions}
  758. size="large"
  759. className="!rounded-lg"
  760. />
  761. </div>
  762. <div className="flex flex-wrap gap-2">
  763. <Button
  764. type='primary'
  765. onClick={() => handleInputChange('models', basicModels)}
  766. size="large"
  767. className="!rounded-lg"
  768. >
  769. {t('填入相关模型')}
  770. </Button>
  771. <Button
  772. type='secondary'
  773. onClick={() => handleInputChange('models', fullModels)}
  774. size="large"
  775. className="!rounded-lg"
  776. >
  777. {t('填入所有模型')}
  778. </Button>
  779. <Button
  780. type='tertiary'
  781. onClick={() => fetchUpstreamModelList('models')}
  782. size="large"
  783. className="!rounded-lg"
  784. >
  785. {t('获取模型列表')}
  786. </Button>
  787. <Button
  788. type='warning'
  789. onClick={() => handleInputChange('models', [])}
  790. size="large"
  791. className="!rounded-lg"
  792. >
  793. {t('清除所有模型')}
  794. </Button>
  795. </div>
  796. <div>
  797. <Input
  798. addonAfter={
  799. <Button type='primary' onClick={addCustomModels} className="!rounded-r-lg">
  800. {t('填入')}
  801. </Button>
  802. }
  803. placeholder={t('输入自定义模型名称')}
  804. value={customModel}
  805. onChange={(value) => setCustomModel(value.trim())}
  806. size="large"
  807. className="!rounded-lg"
  808. />
  809. </div>
  810. <div>
  811. <Text strong className="block mb-2">{t('模型重定向')}</Text>
  812. <TextArea
  813. placeholder={
  814. t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') +
  815. `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`
  816. }
  817. name='model_mapping'
  818. onChange={(value) => handleInputChange('model_mapping', value)}
  819. autosize
  820. value={inputs.model_mapping}
  821. autoComplete='new-password'
  822. className="!rounded-lg font-mono"
  823. />
  824. <Text
  825. className="!text-semi-color-primary cursor-pointer mt-1 block"
  826. onClick={() => handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))}
  827. >
  828. {t('填入模板')}
  829. </Text>
  830. </div>
  831. <div>
  832. <Text strong className="block mb-2">{t('默认测试模型')}</Text>
  833. <Input
  834. name='test_model'
  835. placeholder={t('不填则为模型列表第一个')}
  836. onChange={(value) => handleInputChange('test_model', value)}
  837. value={inputs.test_model}
  838. size="large"
  839. className="!rounded-lg"
  840. />
  841. </div>
  842. </div>
  843. </Card>
  844. {/* Advanced Settings Card */}
  845. <Card className="!rounded-2xl shadow-sm border-0 mb-6">
  846. <div className="flex items-center mb-4 p-6 rounded-xl" style={{
  847. background: 'linear-gradient(135deg, #92400e 0%, #d97706 50%, #f59e0b 100%)',
  848. position: 'relative'
  849. }}>
  850. <div className="absolute inset-0 overflow-hidden">
  851. <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
  852. <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
  853. </div>
  854. <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
  855. <IconSetting size="large" style={{ color: '#ffffff' }} />
  856. </div>
  857. <div className="relative">
  858. <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('高级设置')}</Text>
  859. <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('渠道的高级配置选项')}</div>
  860. </div>
  861. </div>
  862. <div className="space-y-4">
  863. <div>
  864. <Text strong className="block mb-2">{t('分组')}</Text>
  865. <Select
  866. placeholder={t('请选择可以使用该渠道的分组')}
  867. name='groups'
  868. required
  869. multiple
  870. selection
  871. allowAdditions
  872. additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
  873. onChange={(value) => handleInputChange('groups', value)}
  874. value={inputs.groups}
  875. autoComplete='new-password'
  876. optionList={groupOptions}
  877. size="large"
  878. className="!rounded-lg"
  879. />
  880. </div>
  881. {inputs.type === 18 && (
  882. <div>
  883. <Text strong className="block mb-2">{t('模型版本')}</Text>
  884. <Input
  885. name='other'
  886. placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
  887. onChange={(value) => handleInputChange('other', value)}
  888. value={inputs.other}
  889. autoComplete='new-password'
  890. size="large"
  891. className="!rounded-lg"
  892. />
  893. </div>
  894. )}
  895. {inputs.type === 41 && (
  896. <div>
  897. <Text strong className="block mb-2">{t('部署地区')}</Text>
  898. <TextArea
  899. name='other'
  900. placeholder={t(
  901. '请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
  902. '{\n' +
  903. ' "default": "us-central1",\n' +
  904. ' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
  905. '}'
  906. )}
  907. autosize={{ minRows: 2 }}
  908. onChange={(value) => handleInputChange('other', value)}
  909. value={inputs.other}
  910. autoComplete='new-password'
  911. className="!rounded-lg font-mono"
  912. />
  913. <Text
  914. className="!text-semi-color-primary cursor-pointer mt-1 block"
  915. onClick={() => handleInputChange('other', JSON.stringify(REGION_EXAMPLE, null, 2))}
  916. >
  917. {t('填入模板')}
  918. </Text>
  919. </div>
  920. )}
  921. {inputs.type === 21 && (
  922. <div>
  923. <Text strong className="block mb-2">{t('知识库 ID')}</Text>
  924. <Input
  925. name='other'
  926. placeholder={'请输入知识库 ID,例如:123456'}
  927. onChange={(value) => handleInputChange('other', value)}
  928. value={inputs.other}
  929. autoComplete='new-password'
  930. size="large"
  931. className="!rounded-lg"
  932. />
  933. </div>
  934. )}
  935. {inputs.type === 39 && (
  936. <div>
  937. <Text strong className="block mb-2">Account ID</Text>
  938. <Input
  939. name='other'
  940. placeholder={'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'}
  941. onChange={(value) => handleInputChange('other', value)}
  942. value={inputs.other}
  943. autoComplete='new-password'
  944. size="large"
  945. className="!rounded-lg"
  946. />
  947. </div>
  948. )}
  949. {inputs.type === 49 && (
  950. <div>
  951. <Text strong className="block mb-2">{t('智能体ID')}</Text>
  952. <Input
  953. name='other'
  954. placeholder={'请输入智能体ID,例如:7342866812345'}
  955. onChange={(value) => handleInputChange('other', value)}
  956. value={inputs.other}
  957. autoComplete='new-password'
  958. size="large"
  959. className="!rounded-lg"
  960. />
  961. </div>
  962. )}
  963. <div>
  964. <Text strong className="block mb-2">{t('渠道标签')}</Text>
  965. <Input
  966. name='tag'
  967. placeholder={t('渠道标签')}
  968. onChange={(value) => handleInputChange('tag', value)}
  969. value={inputs.tag}
  970. autoComplete='new-password'
  971. size="large"
  972. className="!rounded-lg"
  973. />
  974. </div>
  975. <div>
  976. <Text strong className="block mb-2">{t('渠道优先级')}</Text>
  977. <Input
  978. name='priority'
  979. placeholder={t('渠道优先级')}
  980. onChange={(value) => {
  981. const number = parseInt(value);
  982. if (isNaN(number)) {
  983. handleInputChange('priority', value);
  984. } else {
  985. handleInputChange('priority', number);
  986. }
  987. }}
  988. value={inputs.priority}
  989. autoComplete='new-password'
  990. size="large"
  991. className="!rounded-lg"
  992. />
  993. </div>
  994. <div>
  995. <Text strong className="block mb-2">{t('渠道权重')}</Text>
  996. <Input
  997. name='weight'
  998. placeholder={t('渠道权重')}
  999. onChange={(value) => {
  1000. const number = parseInt(value);
  1001. if (isNaN(number)) {
  1002. handleInputChange('weight', value);
  1003. } else {
  1004. handleInputChange('weight', number);
  1005. }
  1006. }}
  1007. value={inputs.weight}
  1008. autoComplete='new-password'
  1009. size="large"
  1010. className="!rounded-lg"
  1011. />
  1012. </div>
  1013. <div>
  1014. <Text strong className="block mb-2">{t('渠道额外设置')}</Text>
  1015. <TextArea
  1016. placeholder={
  1017. t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') +
  1018. '\n{\n "force_format": true\n}'
  1019. }
  1020. name='setting'
  1021. onChange={(value) => handleInputChange('setting', value)}
  1022. autosize
  1023. value={inputs.setting}
  1024. autoComplete='new-password'
  1025. className="!rounded-lg font-mono"
  1026. />
  1027. <div className="flex gap-2 mt-1">
  1028. <Text
  1029. className="!text-semi-color-primary cursor-pointer"
  1030. onClick={() => {
  1031. handleInputChange(
  1032. 'setting',
  1033. JSON.stringify({ force_format: true }, null, 2),
  1034. );
  1035. }}
  1036. >
  1037. {t('填入模板')}
  1038. </Text>
  1039. <Text
  1040. className="!text-semi-color-primary cursor-pointer"
  1041. onClick={() => {
  1042. window.open(
  1043. 'https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md',
  1044. );
  1045. }}
  1046. >
  1047. {t('设置说明')}
  1048. </Text>
  1049. </div>
  1050. </div>
  1051. <div>
  1052. <Text strong className="block mb-2">{t('参数覆盖')}</Text>
  1053. <TextArea
  1054. placeholder={
  1055. t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') +
  1056. '\n{\n "temperature": 0\n}'
  1057. }
  1058. name='param_override'
  1059. onChange={(value) => handleInputChange('param_override', value)}
  1060. autosize
  1061. value={inputs.param_override}
  1062. autoComplete='new-password'
  1063. className="!rounded-lg font-mono"
  1064. />
  1065. </div>
  1066. {inputs.type === 1 && (
  1067. <div>
  1068. <Text strong className="block mb-2">{t('组织')}</Text>
  1069. <Input
  1070. name='openai_organization'
  1071. placeholder={t('请输入组织org-xxx')}
  1072. onChange={(value) => handleInputChange('openai_organization', value)}
  1073. value={inputs.openai_organization}
  1074. size="large"
  1075. className="!rounded-lg"
  1076. />
  1077. <Text type="tertiary" className="mt-1 text-xs">
  1078. {t('组织,可选,不填则为默认组织')}
  1079. </Text>
  1080. </div>
  1081. )}
  1082. <div className="flex items-center">
  1083. <Checkbox
  1084. checked={autoBan}
  1085. onChange={() => setAutoBan(!autoBan)}
  1086. />
  1087. <Text strong className="ml-2">
  1088. {t('是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道')}
  1089. </Text>
  1090. </div>
  1091. <div>
  1092. <Text strong className="block mb-2">
  1093. {t('状态码复写(仅影响本地判断,不修改返回到上游的状态码)')}
  1094. </Text>
  1095. <TextArea
  1096. placeholder={
  1097. t('此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:') +
  1098. '\n' +
  1099. JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
  1100. }
  1101. name='status_code_mapping'
  1102. onChange={(value) => handleInputChange('status_code_mapping', value)}
  1103. autosize
  1104. value={inputs.status_code_mapping}
  1105. autoComplete='new-password'
  1106. className="!rounded-lg font-mono"
  1107. />
  1108. <Text
  1109. className="!text-semi-color-primary cursor-pointer mt-1 block"
  1110. onClick={() => {
  1111. handleInputChange(
  1112. 'status_code_mapping',
  1113. JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2),
  1114. );
  1115. }}
  1116. >
  1117. {t('填入模板')}
  1118. </Text>
  1119. </div>
  1120. </div>
  1121. </Card>
  1122. </div>
  1123. </Spin>
  1124. <ImagePreview
  1125. src={modalImageUrl}
  1126. visible={isModalOpenurl}
  1127. onVisibleChange={(visible) => setIsModalOpenurl(visible)}
  1128. />
  1129. </SideSheet>
  1130. </>
  1131. );
  1132. };
  1133. export default EditChannel;