EditChannel.js 26 KB

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