EditChannel.js 38 KB

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