EditChannel.js 33 KB

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