EditChannel.js 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117
  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. } from '@douyinfe/semi-ui';
  29. import { getChannelModels, loadChannelModels } from '../../components/utils.js';
  30. const MODEL_MAPPING_EXAMPLE = {
  31. 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
  32. };
  33. const STATUS_CODE_MAPPING_EXAMPLE = {
  34. 400: '500',
  35. };
  36. const REGION_EXAMPLE = {
  37. default: 'us-central1',
  38. 'claude-3-5-sonnet-20240620': 'europe-west1',
  39. };
  40. function type2secretPrompt(type) {
  41. // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
  42. switch (type) {
  43. case 15:
  44. return '按照如下格式输入:APIKey|SecretKey';
  45. case 18:
  46. return '按照如下格式输入:APPID|APISecret|APIKey';
  47. case 22:
  48. return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
  49. case 23:
  50. return '按照如下格式输入:AppId|SecretId|SecretKey';
  51. case 33:
  52. return '按照如下格式输入:Ak|Sk|Region';
  53. default:
  54. return '请输入渠道对应的鉴权密钥';
  55. }
  56. }
  57. const EditChannel = (props) => {
  58. const { t } = useTranslation();
  59. const navigate = useNavigate();
  60. const channelId = props.editingChannel.id;
  61. const isEdit = channelId !== undefined;
  62. const [loading, setLoading] = useState(isEdit);
  63. const handleCancel = () => {
  64. props.handleClose();
  65. };
  66. const originInputs = {
  67. name: '',
  68. type: 1,
  69. key: '',
  70. openai_organization: '',
  71. max_input_tokens: 0,
  72. base_url: '',
  73. other: '',
  74. model_mapping: '',
  75. status_code_mapping: '',
  76. models: [],
  77. auto_ban: 1,
  78. test_model: '',
  79. groups: ['default'],
  80. priority: 0,
  81. weight: 0,
  82. tag: '',
  83. };
  84. const [batch, setBatch] = useState(false);
  85. const [autoBan, setAutoBan] = useState(true);
  86. // const [autoBan, setAutoBan] = useState(true);
  87. const [inputs, setInputs] = useState(originInputs);
  88. const [originModelOptions, setOriginModelOptions] = useState([]);
  89. const [modelOptions, setModelOptions] = useState([]);
  90. const [groupOptions, setGroupOptions] = useState([]);
  91. const [basicModels, setBasicModels] = useState([]);
  92. const [fullModels, setFullModels] = useState([]);
  93. const [customModel, setCustomModel] = useState('');
  94. const handleInputChange = (name, value) => {
  95. if (name === 'base_url' && value.endsWith('/v1')) {
  96. Modal.confirm({
  97. title: '警告',
  98. content:
  99. '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
  100. onOk: () => {
  101. setInputs((inputs) => ({ ...inputs, [name]: value }));
  102. },
  103. });
  104. return;
  105. }
  106. setInputs((inputs) => ({ ...inputs, [name]: value }));
  107. if (name === 'type') {
  108. let localModels = [];
  109. switch (value) {
  110. case 2:
  111. localModels = [
  112. 'mj_imagine',
  113. 'mj_variation',
  114. 'mj_reroll',
  115. 'mj_blend',
  116. 'mj_upscale',
  117. 'mj_describe',
  118. 'mj_uploads',
  119. ];
  120. break;
  121. case 5:
  122. localModels = [
  123. 'swap_face',
  124. 'mj_imagine',
  125. 'mj_variation',
  126. 'mj_reroll',
  127. 'mj_blend',
  128. 'mj_upscale',
  129. 'mj_describe',
  130. 'mj_zoom',
  131. 'mj_shorten',
  132. 'mj_modal',
  133. 'mj_inpaint',
  134. 'mj_custom_zoom',
  135. 'mj_high_variation',
  136. 'mj_low_variation',
  137. 'mj_pan',
  138. 'mj_uploads',
  139. ];
  140. break;
  141. case 36:
  142. localModels = ['suno_music', 'suno_lyrics'];
  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}>
  385. {isEdit ? t('更新渠道信息') : t('创建新的渠道')}
  386. </Title>
  387. }
  388. headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
  389. bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
  390. visible={props.visible}
  391. footer={
  392. <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
  393. <Space>
  394. <Button theme='solid' size={'large'} onClick={submit}>
  395. {t('提交')}
  396. </Button>
  397. <Button
  398. theme='solid'
  399. size={'large'}
  400. type={'tertiary'}
  401. onClick={handleCancel}
  402. >
  403. {t('取消')}
  404. </Button>
  405. </Space>
  406. </div>
  407. }
  408. closeIcon={null}
  409. onCancel={() => handleCancel()}
  410. width={isMobile() ? '100%' : 600}
  411. >
  412. <Spin spinning={loading}>
  413. <div style={{ marginTop: 10 }}>
  414. <Typography.Text strong>{t('类型')}:</Typography.Text>
  415. </div>
  416. <Select
  417. name='type'
  418. required
  419. optionList={CHANNEL_OPTIONS}
  420. value={inputs.type}
  421. onChange={(value) => handleInputChange('type', value)}
  422. style={{ width: '50%' }}
  423. filter
  424. searchPosition='dropdown'
  425. placeholder={t('请选择渠道类型')}
  426. />
  427. {inputs.type === 40 && (
  428. <div style={{ marginTop: 10 }}>
  429. <Banner
  430. type='info'
  431. description={
  432. <div>
  433. <Typography.Text strong>{t('邀请链接')}:</Typography.Text>
  434. <Typography.Text
  435. link
  436. underline
  437. style={{ marginLeft: 8 }}
  438. onClick={() =>
  439. window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')
  440. }
  441. >
  442. https://cloud.siliconflow.cn/i/hij0YNTZ
  443. </Typography.Text>
  444. </div>
  445. }
  446. />
  447. </div>
  448. )}
  449. {inputs.type === 3 && (
  450. <>
  451. <div style={{ marginTop: 10 }}>
  452. <Banner
  453. type={'warning'}
  454. description={t('注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.5-preview会请求为gpt-45-preview,所以部署的模型名称需要去掉点')}
  455. ></Banner>
  456. </div>
  457. <div style={{ marginTop: 10 }}>
  458. <Typography.Text strong>
  459. AZURE_OPENAI_ENDPOINT:
  460. </Typography.Text>
  461. </div>
  462. <Input
  463. label='AZURE_OPENAI_ENDPOINT'
  464. name='azure_base_url'
  465. placeholder={t(
  466. '请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com',
  467. )}
  468. onChange={(value) => {
  469. handleInputChange('base_url', value);
  470. }}
  471. value={inputs.base_url}
  472. autoComplete='new-password'
  473. />
  474. <div style={{ marginTop: 10 }}>
  475. <Typography.Text strong>{t('默认 API 版本')}:</Typography.Text>
  476. </div>
  477. <Input
  478. label={t('默认 API 版本')}
  479. name='azure_other'
  480. placeholder={t('请输入默认 API 版本,例如:2024-12-01-preview')}
  481. onChange={(value) => {
  482. handleInputChange('other', value);
  483. }}
  484. value={inputs.other}
  485. autoComplete='new-password'
  486. />
  487. </>
  488. )}
  489. {inputs.type === 8 && (
  490. <>
  491. <div style={{ marginTop: 10 }}>
  492. <Banner
  493. type={'warning'}
  494. description={t(
  495. '如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。',
  496. )}
  497. ></Banner>
  498. </div>
  499. <div style={{ marginTop: 10 }}>
  500. <Typography.Text strong>
  501. {t('完整的 Base URL,支持变量{model}')}:
  502. </Typography.Text>
  503. </div>
  504. <Input
  505. name='base_url'
  506. placeholder={t(
  507. '请输入完整的URL,例如:https://api.openai.com/v1/chat/completions',
  508. )}
  509. onChange={(value) => {
  510. handleInputChange('base_url', value);
  511. }}
  512. value={inputs.base_url}
  513. autoComplete='new-password'
  514. />
  515. </>
  516. )}
  517. {inputs.type === 37 && (
  518. <>
  519. <div style={{ marginTop: 10 }}>
  520. <Banner
  521. type={'warning'}
  522. description={t(
  523. 'Dify渠道只适配chatflow和agent,并且agent不支持图片!',
  524. )}
  525. ></Banner>
  526. </div>
  527. </>
  528. )}
  529. <div style={{ marginTop: 10 }}>
  530. <Typography.Text strong>{t('名称')}:</Typography.Text>
  531. </div>
  532. <Input
  533. required
  534. name='name'
  535. placeholder={t('请为渠道命名')}
  536. onChange={(value) => {
  537. handleInputChange('name', value);
  538. }}
  539. value={inputs.name}
  540. autoComplete='new-password'
  541. />
  542. {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
  543. <>
  544. <div style={{ marginTop: 10 }}>
  545. <Typography.Text strong>{t('API地址')}:</Typography.Text>
  546. </div>
  547. <Tooltip content={t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')}>
  548. <Input
  549. label={t('API地址')}
  550. name="base_url"
  551. placeholder={t('此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/')}
  552. onChange={(value) => {
  553. handleInputChange('base_url', value);
  554. }}
  555. value={inputs.base_url}
  556. autoComplete="new-password"
  557. />
  558. </Tooltip>
  559. </>
  560. )}
  561. <div style={{ marginTop: 10 }}>
  562. <Typography.Text strong>{t('密钥')}:</Typography.Text>
  563. </div>
  564. {batch ? (
  565. <TextArea
  566. label={t('密钥')}
  567. name='key'
  568. required
  569. placeholder={t('请输入密钥,一行一个')}
  570. onChange={(value) => {
  571. handleInputChange('key', value);
  572. }}
  573. value={inputs.key}
  574. style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
  575. autoComplete='new-password'
  576. />
  577. ) : (
  578. <>
  579. {inputs.type === 41 ? (
  580. <TextArea
  581. label={t('鉴权json')}
  582. name='key'
  583. required
  584. placeholder={
  585. '{\n' +
  586. ' "type": "service_account",\n' +
  587. ' "project_id": "abc-bcd-123-456",\n' +
  588. ' "private_key_id": "123xxxxx456",\n' +
  589. ' "private_key": "-----BEGIN PRIVATE KEY-----xxxx\n' +
  590. ' "client_email": "xxx@developer.gserviceaccount.com",\n' +
  591. ' "client_id": "111222333",\n' +
  592. ' "auth_uri": "https://accounts.google.com/o/oauth2/auth",\n' +
  593. ' "token_uri": "https://oauth2.googleapis.com/token",\n' +
  594. ' "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
  595. ' "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
  596. ' "universe_domain": "googleapis.com"\n' +
  597. '}'
  598. }
  599. onChange={(value) => {
  600. handleInputChange('key', value);
  601. }}
  602. autosize={{ minRows: 10 }}
  603. value={inputs.key}
  604. autoComplete='new-password'
  605. />
  606. ) : (
  607. <Input
  608. label={t('密钥')}
  609. name='key'
  610. required
  611. placeholder={t(type2secretPrompt(inputs.type))}
  612. onChange={(value) => {
  613. handleInputChange('key', value);
  614. }}
  615. value={inputs.key}
  616. autoComplete='new-password'
  617. />
  618. )}
  619. </>
  620. )}
  621. {!isEdit && (
  622. <div style={{ marginTop: 10, display: 'flex' }}>
  623. <Space>
  624. <Checkbox
  625. checked={batch}
  626. label={t('批量创建')}
  627. name='batch'
  628. onChange={() => setBatch(!batch)}
  629. />
  630. <Typography.Text strong>{t('批量创建')}</Typography.Text>
  631. </Space>
  632. </div>
  633. )}
  634. {inputs.type === 22 && (
  635. <>
  636. <div style={{ marginTop: 10 }}>
  637. <Typography.Text strong>{t('私有部署地址')}:</Typography.Text>
  638. </div>
  639. <Input
  640. name='base_url'
  641. placeholder={t(
  642. '请输入私有部署地址,格式为:https://fastgpt.run/api/openapi',
  643. )}
  644. onChange={(value) => {
  645. handleInputChange('base_url', value);
  646. }}
  647. value={inputs.base_url}
  648. autoComplete='new-password'
  649. />
  650. </>
  651. )}
  652. {inputs.type === 36 && (
  653. <>
  654. <div style={{ marginTop: 10 }}>
  655. <Typography.Text strong>
  656. {t(
  657. '注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用',
  658. )}
  659. </Typography.Text>
  660. </div>
  661. <Input
  662. name='base_url'
  663. placeholder={t(
  664. '请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com',
  665. )}
  666. onChange={(value) => {
  667. handleInputChange('base_url', value);
  668. }}
  669. value={inputs.base_url}
  670. autoComplete='new-password'
  671. />
  672. </>
  673. )}
  674. <div style={{ marginTop: 10 }}>
  675. <Typography.Text strong>{t('分组')}:</Typography.Text>
  676. </div>
  677. <Select
  678. placeholder={t('请选择可以使用该渠道的分组')}
  679. name='groups'
  680. required
  681. multiple
  682. selection
  683. allowAdditions
  684. additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
  685. onChange={(value) => {
  686. handleInputChange('groups', value);
  687. }}
  688. value={inputs.groups}
  689. autoComplete='new-password'
  690. optionList={groupOptions}
  691. />
  692. {inputs.type === 18 && (
  693. <>
  694. <div style={{ marginTop: 10 }}>
  695. <Typography.Text strong>模型版本:</Typography.Text>
  696. </div>
  697. <Input
  698. name='other'
  699. placeholder={
  700. '请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
  701. }
  702. onChange={(value) => {
  703. handleInputChange('other', value);
  704. }}
  705. value={inputs.other}
  706. autoComplete='new-password'
  707. />
  708. </>
  709. )}
  710. {inputs.type === 41 && (
  711. <>
  712. <div style={{ marginTop: 10 }}>
  713. <Typography.Text strong>{t('部署地区')}:</Typography.Text>
  714. </div>
  715. <TextArea
  716. name='other'
  717. placeholder={t(
  718. '请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
  719. '{\n' +
  720. ' "default": "us-central1",\n' +
  721. ' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
  722. '}',
  723. )}
  724. autosize={{ minRows: 2 }}
  725. onChange={(value) => {
  726. handleInputChange('other', value);
  727. }}
  728. value={inputs.other}
  729. autoComplete='new-password'
  730. />
  731. <Typography.Text
  732. style={{
  733. color: 'rgba(var(--semi-blue-5), 1)',
  734. userSelect: 'none',
  735. cursor: 'pointer',
  736. }}
  737. onClick={() => {
  738. handleInputChange(
  739. 'other',
  740. JSON.stringify(REGION_EXAMPLE, null, 2),
  741. );
  742. }}
  743. >
  744. {t('填入模板')}
  745. </Typography.Text>
  746. </>
  747. )}
  748. {inputs.type === 21 && (
  749. <>
  750. <div style={{ marginTop: 10 }}>
  751. <Typography.Text strong>知识库 ID:</Typography.Text>
  752. </div>
  753. <Input
  754. label='知识库 ID'
  755. name='other'
  756. placeholder={'请输入知识库 ID,例如:123456'}
  757. onChange={(value) => {
  758. handleInputChange('other', value);
  759. }}
  760. value={inputs.other}
  761. autoComplete='new-password'
  762. />
  763. </>
  764. )}
  765. {inputs.type === 39 && (
  766. <>
  767. <div style={{ marginTop: 10 }}>
  768. <Typography.Text strong>Account ID:</Typography.Text>
  769. </div>
  770. <Input
  771. name='other'
  772. placeholder={
  773. '请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'
  774. }
  775. onChange={(value) => {
  776. handleInputChange('other', value);
  777. }}
  778. value={inputs.other}
  779. autoComplete='new-password'
  780. />
  781. </>
  782. )}
  783. <div style={{ marginTop: 10 }}>
  784. <Typography.Text strong>{t('模型')}:</Typography.Text>
  785. </div>
  786. <Select
  787. placeholder={'请选择该渠道所支持的模型'}
  788. name='models'
  789. required
  790. multiple
  791. selection
  792. filter
  793. searchPosition='dropdown'
  794. onChange={(value) => {
  795. handleInputChange('models', value);
  796. }}
  797. value={inputs.models}
  798. autoComplete='new-password'
  799. optionList={modelOptions}
  800. />
  801. <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
  802. <Space>
  803. <Button
  804. type='primary'
  805. onClick={() => {
  806. handleInputChange('models', basicModels);
  807. }}
  808. >
  809. {t('填入相关模型')}
  810. </Button>
  811. <Button
  812. type='secondary'
  813. onClick={() => {
  814. handleInputChange('models', fullModels);
  815. }}
  816. >
  817. {t('填入所有模型')}
  818. </Button>
  819. <Tooltip
  820. content={t(
  821. '新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出',
  822. )}
  823. >
  824. <Button
  825. type='tertiary'
  826. onClick={() => {
  827. fetchUpstreamModelList('models');
  828. }}
  829. >
  830. {t('获取模型列表')}
  831. </Button>
  832. </Tooltip>
  833. <Button
  834. type='warning'
  835. onClick={() => {
  836. handleInputChange('models', []);
  837. }}
  838. >
  839. {t('清除所有模型')}
  840. </Button>
  841. </Space>
  842. <Input
  843. addonAfter={
  844. <Button type='primary' onClick={addCustomModels}>
  845. {t('填入')}
  846. </Button>
  847. }
  848. placeholder={t('输入自定义模型名称')}
  849. value={customModel}
  850. onChange={(value) => {
  851. setCustomModel(value.trim());
  852. }}
  853. />
  854. </div>
  855. <div style={{ marginTop: 10 }}>
  856. <Typography.Text strong>{t('模型重定向')}:</Typography.Text>
  857. </div>
  858. <TextArea
  859. placeholder={
  860. t(
  861. '此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:',
  862. ) + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`
  863. }
  864. name='model_mapping'
  865. onChange={(value) => {
  866. handleInputChange('model_mapping', value);
  867. }}
  868. autosize
  869. value={inputs.model_mapping}
  870. autoComplete='new-password'
  871. />
  872. <Typography.Text
  873. style={{
  874. color: 'rgba(var(--semi-blue-5), 1)',
  875. userSelect: 'none',
  876. cursor: 'pointer',
  877. }}
  878. onClick={() => {
  879. handleInputChange(
  880. 'model_mapping',
  881. JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
  882. );
  883. }}
  884. >
  885. {t('填入模板')}
  886. </Typography.Text>
  887. <div style={{ marginTop: 10 }}>
  888. <Typography.Text strong>{t('渠道标签')}</Typography.Text>
  889. </div>
  890. <Input
  891. label={t('渠道标签')}
  892. name='tag'
  893. placeholder={t('渠道标签')}
  894. onChange={(value) => {
  895. handleInputChange('tag', value);
  896. }}
  897. value={inputs.tag}
  898. autoComplete='new-password'
  899. />
  900. <div style={{ marginTop: 10 }}>
  901. <Typography.Text strong>{t('渠道优先级')}</Typography.Text>
  902. </div>
  903. <Input
  904. label={t('渠道优先级')}
  905. name='priority'
  906. placeholder={t('渠道优先级')}
  907. onChange={(value) => {
  908. const number = parseInt(value);
  909. if (isNaN(number)) {
  910. handleInputChange('priority', value);
  911. } else {
  912. handleInputChange('priority', number);
  913. }
  914. }}
  915. value={inputs.priority}
  916. autoComplete='new-password'
  917. />
  918. <div style={{ marginTop: 10 }}>
  919. <Typography.Text strong>{t('渠道权重')}</Typography.Text>
  920. </div>
  921. <Input
  922. label={t('渠道权重')}
  923. name='weight'
  924. placeholder={t('渠道权重')}
  925. onChange={(value) => {
  926. const number = parseInt(value);
  927. if (isNaN(number)) {
  928. handleInputChange('weight', value);
  929. } else {
  930. handleInputChange('weight', number);
  931. }
  932. }}
  933. value={inputs.weight}
  934. autoComplete='new-password'
  935. />
  936. <>
  937. <div style={{ marginTop: 10 }}>
  938. <Typography.Text strong>{t('渠道额外设置')}:</Typography.Text>
  939. </div>
  940. <TextArea
  941. placeholder={
  942. t(
  943. '此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:',
  944. ) + '\n{\n "force_format": true\n}'
  945. }
  946. name='setting'
  947. onChange={(value) => {
  948. handleInputChange('setting', value);
  949. }}
  950. autosize
  951. value={inputs.setting}
  952. autoComplete='new-password'
  953. />
  954. <Space>
  955. <Typography.Text
  956. style={{
  957. color: 'rgba(var(--semi-blue-5), 1)',
  958. userSelect: 'none',
  959. cursor: 'pointer',
  960. }}
  961. onClick={() => {
  962. handleInputChange(
  963. 'setting',
  964. JSON.stringify(
  965. {
  966. force_format: true,
  967. },
  968. null,
  969. 2,
  970. ),
  971. );
  972. }}
  973. >
  974. {t('填入模板')}
  975. </Typography.Text>
  976. <Typography.Text
  977. style={{
  978. color: 'rgba(var(--semi-blue-5), 1)',
  979. userSelect: 'none',
  980. cursor: 'pointer',
  981. }}
  982. onClick={() => {
  983. window.open(
  984. 'https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md',
  985. );
  986. }}
  987. >
  988. {t('设置说明')}
  989. </Typography.Text>
  990. </Space>
  991. </>
  992. <>
  993. <div style={{ marginTop: 10 }}>
  994. <Typography.Text strong>{t('参数覆盖')}:</Typography.Text>
  995. </div>
  996. <TextArea
  997. placeholder={
  998. t(
  999. '此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:',
  1000. ) + '\n{\n "temperature": 0\n}'
  1001. }
  1002. name='setting'
  1003. onChange={(value) => {
  1004. handleInputChange('param_override', value);
  1005. }}
  1006. autosize
  1007. value={inputs.param_override}
  1008. autoComplete='new-password'
  1009. />
  1010. </>
  1011. {inputs.type === 1 && (
  1012. <>
  1013. <div style={{ marginTop: 10 }}>
  1014. <Typography.Text strong>{t('组织')}:</Typography.Text>
  1015. </div>
  1016. <Input
  1017. label={t('组织,可选,不填则为默认组织')}
  1018. name='openai_organization'
  1019. placeholder={t('请输入组织org-xxx')}
  1020. onChange={(value) => {
  1021. handleInputChange('openai_organization', value);
  1022. }}
  1023. value={inputs.openai_organization}
  1024. />
  1025. </>
  1026. )}
  1027. <div style={{ marginTop: 10 }}>
  1028. <Typography.Text strong>{t('默认测试模型')}:</Typography.Text>
  1029. </div>
  1030. <Input
  1031. name='test_model'
  1032. placeholder={t('不填则为模型列表第一个')}
  1033. onChange={(value) => {
  1034. handleInputChange('test_model', value);
  1035. }}
  1036. value={inputs.test_model}
  1037. />
  1038. <div style={{ marginTop: 10, display: 'flex' }}>
  1039. <Space>
  1040. <Checkbox
  1041. name='auto_ban'
  1042. checked={autoBan}
  1043. onChange={() => {
  1044. setAutoBan(!autoBan);
  1045. }}
  1046. />
  1047. <Typography.Text strong>
  1048. {t(
  1049. '是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:',
  1050. )}
  1051. </Typography.Text>
  1052. </Space>
  1053. </div>
  1054. <div style={{ marginTop: 10 }}>
  1055. <Typography.Text strong>
  1056. {t('状态码复写(仅影响本地判断,不修改返回到上游的状态码)')}:
  1057. </Typography.Text>
  1058. </div>
  1059. <TextArea
  1060. placeholder={
  1061. t(
  1062. '此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:',
  1063. ) +
  1064. '\n' +
  1065. JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
  1066. }
  1067. name='status_code_mapping'
  1068. onChange={(value) => {
  1069. handleInputChange('status_code_mapping', value);
  1070. }}
  1071. autosize
  1072. value={inputs.status_code_mapping}
  1073. autoComplete='new-password'
  1074. />
  1075. <Typography.Text
  1076. style={{
  1077. color: 'rgba(var(--semi-blue-5), 1)',
  1078. userSelect: 'none',
  1079. cursor: 'pointer',
  1080. }}
  1081. onClick={() => {
  1082. handleInputChange(
  1083. 'status_code_mapping',
  1084. JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2),
  1085. );
  1086. }}
  1087. >
  1088. {t('填入模板')}
  1089. </Typography.Text>
  1090. </Spin>
  1091. </SideSheet>
  1092. </>
  1093. );
  1094. };
  1095. export default EditChannel;