EditChannel.js 38 KB

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