EditChannel.js 46 KB

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