EditChannel.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. import React, {useEffect, useRef, useState} from 'react';
  2. import {useNavigate, useParams} from 'react-router-dom';
  3. import {API, isMobile, showError, showInfo, showSuccess, verifyJSON} from '../../helpers';
  4. import {CHANNEL_OPTIONS} from '../../constants';
  5. import Title from "@douyinfe/semi-ui/lib/es/typography/title";
  6. import {SideSheet, Space, Spin, Button, Input, Typography, Select, TextArea, Checkbox, Banner} from "@douyinfe/semi-ui";
  7. const MODEL_MAPPING_EXAMPLE = {
  8. 'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
  9. 'gpt-4-0314': 'gpt-4',
  10. 'gpt-4-32k-0314': 'gpt-4-32k'
  11. };
  12. function type2secretPrompt(type) {
  13. // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
  14. switch (type) {
  15. case 15:
  16. return '按照如下格式输入:APIKey|SecretKey';
  17. case 18:
  18. return '按照如下格式输入:APPID|APISecret|APIKey';
  19. case 22:
  20. return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
  21. case 23:
  22. return '按照如下格式输入:AppId|SecretId|SecretKey';
  23. default:
  24. return '请输入渠道对应的鉴权密钥';
  25. }
  26. }
  27. const EditChannel = (props) => {
  28. const navigate = useNavigate();
  29. const channelId = props.editingChannel.id;
  30. const isEdit = channelId !== undefined;
  31. const [loading, setLoading] = useState(isEdit);
  32. const handleCancel = () => {
  33. props.handleClose()
  34. };
  35. const originInputs = {
  36. name: '',
  37. type: 1,
  38. key: '',
  39. openai_organization: '',
  40. base_url: '',
  41. other: '',
  42. model_mapping: '',
  43. models: [],
  44. auto_ban: 1,
  45. groups: ['default']
  46. };
  47. const [batch, setBatch] = useState(false);
  48. const [autoBan, setAutoBan] = useState(true);
  49. // const [autoBan, setAutoBan] = useState(true);
  50. const [inputs, setInputs] = useState(originInputs);
  51. const [originModelOptions, setOriginModelOptions] = useState([]);
  52. const [modelOptions, setModelOptions] = useState([]);
  53. const [groupOptions, setGroupOptions] = useState([]);
  54. const [basicModels, setBasicModels] = useState([]);
  55. const [fullModels, setFullModels] = useState([]);
  56. const [customModel, setCustomModel] = useState('');
  57. const handleInputChange = (name, value) => {
  58. setInputs((inputs) => ({...inputs, [name]: value}));
  59. if (name === 'type' && inputs.models.length === 0) {
  60. let localModels = [];
  61. switch (value) {
  62. case 14:
  63. localModels = ["claude-instant-1.2", "claude-2", "claude-2.0", "claude-2.1", "claude-3-sonnet-20240229", "claude-3-opus-20240229"];
  64. break;
  65. case 11:
  66. localModels = ['PaLM-2'];
  67. break;
  68. case 15:
  69. localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'ERNIE-Bot-4', 'Embedding-V1'];
  70. break;
  71. case 17:
  72. localModels = ["qwen-turbo", "qwen-plus", "qwen-max", "qwen-max-longcontext", 'text-embedding-v1'];
  73. break;
  74. case 16:
  75. localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite'];
  76. break;
  77. case 18:
  78. localModels = ['SparkDesk', 'SparkDesk-v1.1', 'SparkDesk-v2.1', 'SparkDesk-v3.1', 'SparkDesk-v3.5'];
  79. break;
  80. case 19:
  81. localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1'];
  82. break;
  83. case 23:
  84. localModels = ['hunyuan'];
  85. break;
  86. case 24:
  87. localModels = ['gemini-pro', 'gemini-pro-vision'];
  88. break;
  89. case 25:
  90. localModels = ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'];
  91. break;
  92. case 26:
  93. localModels = ['glm-4', 'glm-4v', 'glm-3-turbo'];
  94. break;
  95. case 2:
  96. localModels = ['midjourney'];
  97. break;
  98. case 5:
  99. localModels = ['midjourney'];
  100. break;
  101. }
  102. setInputs((inputs) => ({...inputs, models: localModels}));
  103. }
  104. //setAutoBan
  105. };
  106. const loadChannel = async () => {
  107. setLoading(true)
  108. let res = await API.get(`/api/channel/${channelId}`);
  109. const {success, message, data} = res.data;
  110. if (success) {
  111. if (data.models === '') {
  112. data.models = [];
  113. } else {
  114. data.models = data.models.split(',');
  115. }
  116. if (data.group === '') {
  117. data.groups = [];
  118. } else {
  119. data.groups = data.group.split(',');
  120. }
  121. if (data.model_mapping !== '') {
  122. data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2);
  123. }
  124. setInputs(data);
  125. if (data.auto_ban === 0) {
  126. setAutoBan(false);
  127. } else {
  128. setAutoBan(true);
  129. }
  130. // console.log(data);
  131. } else {
  132. showError(message);
  133. }
  134. setLoading(false);
  135. };
  136. const fetchModels = async () => {
  137. try {
  138. let res = await API.get(`/api/channel/models`);
  139. let localModelOptions = res.data.data.map((model) => ({
  140. label: model.id,
  141. value: model.id
  142. }));
  143. setOriginModelOptions(localModelOptions);
  144. setFullModels(res.data.data.map((model) => model.id));
  145. setBasicModels(res.data.data.filter((model) => {
  146. return model.id.startsWith('gpt-3') || model.id.startsWith('text-');
  147. }).map((model) => model.id));
  148. } catch (error) {
  149. showError(error.message);
  150. }
  151. };
  152. const fetchGroups = async () => {
  153. try {
  154. let res = await API.get(`/api/group/`);
  155. setGroupOptions(res.data.data.map((group) => ({
  156. label: group,
  157. value: group
  158. })));
  159. } catch (error) {
  160. showError(error.message);
  161. }
  162. };
  163. useEffect(() => {
  164. let localModelOptions = [...originModelOptions];
  165. inputs.models.forEach((model) => {
  166. if (!localModelOptions.find((option) => option.key === model)) {
  167. localModelOptions.push({
  168. label: model,
  169. value: model
  170. });
  171. }
  172. });
  173. setModelOptions(localModelOptions);
  174. }, [originModelOptions, inputs.models]);
  175. useEffect(() => {
  176. fetchModels().then();
  177. fetchGroups().then();
  178. if (isEdit) {
  179. loadChannel().then(
  180. () => {
  181. }
  182. );
  183. } else {
  184. setInputs(originInputs)
  185. }
  186. }, [props.editingChannel.id]);
  187. const submit = async () => {
  188. if (!isEdit && (inputs.name === '' || inputs.key === '')) {
  189. showInfo('请填写渠道名称和渠道密钥!');
  190. return;
  191. }
  192. if (inputs.models.length === 0) {
  193. showInfo('请至少选择一个模型!');
  194. return;
  195. }
  196. if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
  197. showInfo('模型映射必须是合法的 JSON 格式!');
  198. return;
  199. }
  200. let localInputs = {...inputs};
  201. if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
  202. localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
  203. }
  204. if (localInputs.type === 3 && localInputs.other === '') {
  205. localInputs.other = '2023-06-01-preview';
  206. }
  207. if (localInputs.type === 18 && localInputs.other === '') {
  208. localInputs.other = 'v2.1';
  209. }
  210. let res;
  211. if (!Array.isArray(localInputs.models)) {
  212. showError('提交失败,请勿重复提交!');
  213. handleCancel();
  214. return;
  215. }
  216. localInputs.auto_ban = autoBan ? 1 : 0;
  217. localInputs.models = localInputs.models.join(',');
  218. localInputs.group = localInputs.groups.join(',');
  219. if (isEdit) {
  220. res = await API.put(`/api/channel/`, {...localInputs, id: parseInt(channelId)});
  221. } else {
  222. res = await API.post(`/api/channel/`, localInputs);
  223. }
  224. const {success, message} = res.data;
  225. if (success) {
  226. if (isEdit) {
  227. showSuccess('渠道更新成功!');
  228. } else {
  229. showSuccess('渠道创建成功!');
  230. setInputs(originInputs);
  231. }
  232. props.refresh();
  233. props.handleClose();
  234. } else {
  235. showError(message);
  236. }
  237. };
  238. const addCustomModel = () => {
  239. if (customModel.trim() === '') return;
  240. if (inputs.models.includes(customModel)) return showError("该模型已存在!");
  241. let localModels = [...inputs.models];
  242. localModels.push(customModel);
  243. let localModelOptions = [];
  244. localModelOptions.push({
  245. key: customModel,
  246. text: customModel,
  247. value: customModel
  248. });
  249. setModelOptions(modelOptions => {
  250. return [...modelOptions, ...localModelOptions];
  251. });
  252. setCustomModel('');
  253. handleInputChange('models', localModels);
  254. };
  255. return (
  256. <>
  257. <SideSheet
  258. maskClosable={false}
  259. placement={isEdit ? 'right' : 'left'}
  260. title={<Title level={3}>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Title>}
  261. headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
  262. bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
  263. visible={props.visible}
  264. footer={
  265. <div style={{display: 'flex', justifyContent: 'flex-end'}}>
  266. <Space>
  267. <Button theme='solid' size={'large'} onClick={submit}>提交</Button>
  268. <Button theme='solid' size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
  269. </Space>
  270. </div>
  271. }
  272. closeIcon={null}
  273. onCancel={() => handleCancel()}
  274. width={isMobile() ? '100%' : 600}
  275. >
  276. <Spin spinning={loading}>
  277. <div style={{marginTop: 10}}>
  278. <Typography.Text strong>类型:</Typography.Text>
  279. </div>
  280. <Select
  281. name='type'
  282. required
  283. optionList={CHANNEL_OPTIONS}
  284. value={inputs.type}
  285. onChange={value => handleInputChange('type', value)}
  286. style={{width: '50%'}}
  287. />
  288. {
  289. inputs.type === 3 && (
  290. <>
  291. <div style={{marginTop: 10}}>
  292. <Banner type={"warning"} description={
  293. <>
  294. 注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的
  295. model
  296. 参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank'
  297. href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。
  298. </>
  299. }>
  300. </Banner>
  301. </div>
  302. <div style={{marginTop: 10}}>
  303. <Typography.Text strong>AZURE_OPENAI_ENDPOINT:</Typography.Text>
  304. </div>
  305. <Input
  306. label='AZURE_OPENAI_ENDPOINT'
  307. name='azure_base_url'
  308. placeholder={'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'}
  309. onChange={value => {
  310. handleInputChange('base_url', value)
  311. }}
  312. value={inputs.base_url}
  313. autoComplete='new-password'
  314. />
  315. <div style={{marginTop: 10}}>
  316. <Typography.Text strong>默认 API 版本:</Typography.Text>
  317. </div>
  318. <Input
  319. label='默认 API 版本'
  320. name='azure_other'
  321. placeholder={'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'}
  322. onChange={value => {
  323. handleInputChange('other', value)
  324. }}
  325. value={inputs.other}
  326. autoComplete='new-password'
  327. />
  328. </>
  329. )
  330. }
  331. {
  332. inputs.type === 8 && (
  333. <>
  334. <div style={{marginTop: 10}}>
  335. <Typography.Text strong>Base URL:</Typography.Text>
  336. </div>
  337. <Input
  338. name='base_url'
  339. placeholder={'请输入自定义渠道的 Base URL'}
  340. onChange={value => {
  341. handleInputChange('base_url', value)
  342. }}
  343. value={inputs.base_url}
  344. autoComplete='new-password'
  345. />
  346. </>
  347. )
  348. }
  349. <div style={{marginTop: 10}}>
  350. <Typography.Text strong>名称:</Typography.Text>
  351. </div>
  352. <Input
  353. required
  354. name='name'
  355. placeholder={'请为渠道命名'}
  356. onChange={value => {
  357. handleInputChange('name', value)
  358. }}
  359. value={inputs.name}
  360. autoComplete='new-password'
  361. />
  362. <div style={{marginTop: 10}}>
  363. <Typography.Text strong>分组:</Typography.Text>
  364. </div>
  365. <Select
  366. placeholder={'请选择可以使用该渠道的分组'}
  367. name='groups'
  368. required
  369. multiple
  370. selection
  371. allowAdditions
  372. additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
  373. onChange={value => {
  374. handleInputChange('groups', value)
  375. }}
  376. value={inputs.groups}
  377. autoComplete='new-password'
  378. optionList={groupOptions}
  379. />
  380. {
  381. inputs.type === 18 && (
  382. <>
  383. <div style={{marginTop: 10}}>
  384. <Typography.Text strong>模型版本:</Typography.Text>
  385. </div>
  386. <Input
  387. name='other'
  388. placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
  389. onChange={value => {
  390. handleInputChange('other', value)
  391. }}
  392. value={inputs.other}
  393. autoComplete='new-password'
  394. />
  395. </>
  396. )
  397. }
  398. {
  399. inputs.type === 21 && (
  400. <>
  401. <div style={{marginTop: 10}}>
  402. <Typography.Text strong>知识库 ID:</Typography.Text>
  403. </div>
  404. <Input
  405. label='知识库 ID'
  406. name='other'
  407. placeholder={'请输入知识库 ID,例如:123456'}
  408. onChange={value => {
  409. handleInputChange('other', value)
  410. }}
  411. value={inputs.other}
  412. autoComplete='new-password'
  413. />
  414. </>
  415. )
  416. }
  417. <div style={{marginTop: 10}}>
  418. <Typography.Text strong>模型:</Typography.Text>
  419. </div>
  420. <Select
  421. placeholder={'请选择该渠道所支持的模型'}
  422. name='models'
  423. required
  424. multiple
  425. selection
  426. onChange={value => {
  427. handleInputChange('models', value)
  428. }}
  429. value={inputs.models}
  430. autoComplete='new-password'
  431. optionList={modelOptions}
  432. />
  433. <div style={{lineHeight: '40px', marginBottom: '12px'}}>
  434. <Space>
  435. <Button type='primary' onClick={() => {
  436. handleInputChange('models', basicModels);
  437. }}>填入基础模型</Button>
  438. <Button type='secondary' onClick={() => {
  439. handleInputChange('models', fullModels);
  440. }}>填入所有模型</Button>
  441. <Button type='warning' onClick={() => {
  442. handleInputChange('models', []);
  443. }}>清除所有模型</Button>
  444. </Space>
  445. <Input
  446. addonAfter={
  447. <Button type='primary' onClick={addCustomModel}>填入</Button>
  448. }
  449. placeholder='输入自定义模型名称'
  450. value={customModel}
  451. onChange={(value) => {
  452. setCustomModel(value.trim());
  453. }}
  454. />
  455. </div>
  456. <div style={{marginTop: 10}}>
  457. <Typography.Text strong>模型重定向:</Typography.Text>
  458. </div>
  459. <TextArea
  460. placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
  461. name='model_mapping'
  462. onChange={value => {
  463. handleInputChange('model_mapping', value)
  464. }}
  465. autosize
  466. value={inputs.model_mapping}
  467. autoComplete='new-password'
  468. />
  469. <Typography.Text style={{
  470. color: 'rgba(var(--semi-blue-5), 1)',
  471. userSelect: 'none',
  472. cursor: 'pointer'
  473. }} onClick={
  474. () => {
  475. handleInputChange('model_mapping', JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2))
  476. }
  477. }>
  478. 填入模板
  479. </Typography.Text>
  480. <div style={{marginTop: 10}}>
  481. <Typography.Text strong>密钥:</Typography.Text>
  482. </div>
  483. {
  484. batch ?
  485. <TextArea
  486. label='密钥'
  487. name='key'
  488. required
  489. placeholder={'请输入密钥,一行一个'}
  490. onChange={value => {
  491. handleInputChange('key', value)
  492. }}
  493. value={inputs.key}
  494. style={{minHeight: 150, fontFamily: 'JetBrains Mono, Consolas'}}
  495. autoComplete='new-password'
  496. />
  497. :
  498. <Input
  499. label='密钥'
  500. name='key'
  501. required
  502. placeholder={type2secretPrompt(inputs.type)}
  503. onChange={value => {
  504. handleInputChange('key', value)
  505. }}
  506. value={inputs.key}
  507. autoComplete='new-password'
  508. />
  509. }
  510. <div style={{marginTop: 10}}>
  511. <Typography.Text strong>组织:</Typography.Text>
  512. </div>
  513. <Input
  514. label='组织,可选,不填则为默认组织'
  515. name='openai_organization'
  516. placeholder='请输入组织org-xxx'
  517. onChange={value => {
  518. handleInputChange('openai_organization', value)
  519. }}
  520. value={inputs.openai_organization}
  521. />
  522. <div style={{marginTop: 10, display: 'flex'}}>
  523. <Space>
  524. <Checkbox
  525. name='auto_ban'
  526. checked={autoBan}
  527. onChange={
  528. () => {
  529. setAutoBan(!autoBan);
  530. }
  531. }
  532. // onChange={handleInputChange}
  533. />
  534. <Typography.Text
  535. strong>是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:</Typography.Text>
  536. </Space>
  537. </div>
  538. {
  539. !isEdit && (
  540. <div style={{marginTop: 10, display: 'flex'}}>
  541. <Space>
  542. <Checkbox
  543. checked={batch}
  544. label='批量创建'
  545. name='batch'
  546. onChange={() => setBatch(!batch)}
  547. />
  548. <Typography.Text strong>批量创建</Typography.Text>
  549. </Space>
  550. </div>
  551. )
  552. }
  553. {
  554. inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && (
  555. <>
  556. <div style={{marginTop: 10}}>
  557. <Typography.Text strong>代理:</Typography.Text>
  558. </div>
  559. <Input
  560. label='代理'
  561. name='base_url'
  562. placeholder={'此项可选,用于通过代理站来进行 API 调用'}
  563. onChange={value => {
  564. handleInputChange('base_url', value)
  565. }}
  566. value={inputs.base_url}
  567. autoComplete='new-password'
  568. />
  569. </>
  570. )
  571. }
  572. {
  573. inputs.type === 22 && (
  574. <>
  575. <div style={{marginTop: 10}}>
  576. <Typography.Text strong>私有部署地址:</Typography.Text>
  577. </div>
  578. <Input
  579. name='base_url'
  580. placeholder={'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'}
  581. onChange={value => {
  582. handleInputChange('base_url', value)
  583. }}
  584. value={inputs.base_url}
  585. autoComplete='new-password'
  586. />
  587. </>
  588. )
  589. }
  590. </Spin>
  591. </SideSheet>
  592. </>
  593. );
  594. };
  595. export default EditChannel;