EditChannel.js 34 KB

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