EditChannel.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. import React, { useEffect, useState } from 'react';
  2. import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react';
  3. import { useNavigate, useParams } from 'react-router-dom';
  4. import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers';
  5. import { CHANNEL_OPTIONS } from '../../constants';
  6. const MODEL_MAPPING_EXAMPLE = {
  7. 'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
  8. 'gpt-4-0314': 'gpt-4',
  9. 'gpt-4-32k-0314': 'gpt-4-32k'
  10. };
  11. function type2secretPrompt(type) {
  12. // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
  13. switch (type) {
  14. case 15:
  15. return '按照如下格式输入:APIKey|SecretKey';
  16. case 18:
  17. return '按照如下格式输入:APPID|APISecret|APIKey';
  18. case 22:
  19. return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
  20. default:
  21. return '请输入渠道对应的鉴权密钥';
  22. }
  23. }
  24. const EditChannel = () => {
  25. const params = useParams();
  26. const navigate = useNavigate();
  27. const channelId = params.id;
  28. const isEdit = channelId !== undefined;
  29. const [loading, setLoading] = useState(isEdit);
  30. const handleCancel = () => {
  31. navigate('/channel');
  32. };
  33. const originInputs = {
  34. name: '',
  35. type: 1,
  36. key: '',
  37. base_url: '',
  38. other: '',
  39. model_mapping: '',
  40. models: [],
  41. groups: ['default']
  42. };
  43. const [batch, setBatch] = useState(false);
  44. const [inputs, setInputs] = useState(originInputs);
  45. const [originModelOptions, setOriginModelOptions] = useState([]);
  46. const [modelOptions, setModelOptions] = useState([]);
  47. const [groupOptions, setGroupOptions] = useState([]);
  48. const [basicModels, setBasicModels] = useState([]);
  49. const [fullModels, setFullModels] = useState([]);
  50. const [customModel, setCustomModel] = useState('');
  51. const handleInputChange = (e, { name, value }) => {
  52. setInputs((inputs) => ({ ...inputs, [name]: value }));
  53. if (name === 'type' && inputs.models.length === 0) {
  54. let localModels = [];
  55. switch (value) {
  56. case 14:
  57. localModels = ['claude-instant-1', 'claude-2'];
  58. break;
  59. case 11:
  60. localModels = ['PaLM-2'];
  61. break;
  62. case 15:
  63. localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'Embedding-V1'];
  64. break;
  65. case 17:
  66. localModels = ['qwen-v1', 'qwen-plus-v1'];
  67. break;
  68. case 16:
  69. localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite'];
  70. break;
  71. case 18:
  72. localModels = ['SparkDesk'];
  73. break;
  74. case 19:
  75. localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1', '360GPT_S2_V9.4'];
  76. break;
  77. }
  78. setInputs((inputs) => ({ ...inputs, models: localModels }));
  79. }
  80. };
  81. const loadChannel = async () => {
  82. let res = await API.get(`/api/channel/${channelId}`);
  83. const { success, message, data } = res.data;
  84. if (success) {
  85. if (data.models === '') {
  86. data.models = [];
  87. } else {
  88. data.models = data.models.split(',');
  89. }
  90. if (data.group === '') {
  91. data.groups = [];
  92. } else {
  93. data.groups = data.group.split(',');
  94. }
  95. if (data.model_mapping !== '') {
  96. data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2);
  97. }
  98. setInputs(data);
  99. } else {
  100. showError(message);
  101. }
  102. setLoading(false);
  103. };
  104. const fetchModels = async () => {
  105. try {
  106. let res = await API.get(`/api/channel/models`);
  107. let localModelOptions = res.data.data.map((model) => ({
  108. key: model.id,
  109. text: model.id,
  110. value: model.id
  111. }));
  112. setOriginModelOptions(localModelOptions);
  113. setFullModels(res.data.data.map((model) => model.id));
  114. setBasicModels(res.data.data.filter((model) => {
  115. return model.id.startsWith('gpt-3') || model.id.startsWith('text-');
  116. }).map((model) => model.id));
  117. } catch (error) {
  118. showError(error.message);
  119. }
  120. };
  121. const fetchGroups = async () => {
  122. try {
  123. let res = await API.get(`/api/group/`);
  124. setGroupOptions(res.data.data.map((group) => ({
  125. key: group,
  126. text: group,
  127. value: group
  128. })));
  129. } catch (error) {
  130. showError(error.message);
  131. }
  132. };
  133. useEffect(() => {
  134. let localModelOptions = [...originModelOptions];
  135. inputs.models.forEach((model) => {
  136. if (!localModelOptions.find((option) => option.key === model)) {
  137. localModelOptions.push({
  138. key: model,
  139. text: model,
  140. value: model
  141. });
  142. }
  143. });
  144. setModelOptions(localModelOptions);
  145. }, [originModelOptions, inputs.models]);
  146. useEffect(() => {
  147. if (isEdit) {
  148. loadChannel().then();
  149. }
  150. fetchModels().then();
  151. fetchGroups().then();
  152. }, []);
  153. const submit = async () => {
  154. if (!isEdit && (inputs.name === '' || inputs.key === '')) {
  155. showInfo('请填写渠道名称和渠道密钥!');
  156. return;
  157. }
  158. if (inputs.models.length === 0) {
  159. showInfo('请至少选择一个模型!');
  160. return;
  161. }
  162. if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
  163. showInfo('模型映射必须是合法的 JSON 格式!');
  164. return;
  165. }
  166. let localInputs = inputs;
  167. if (localInputs.base_url.endsWith('/')) {
  168. localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
  169. }
  170. if (localInputs.type === 3 && localInputs.other === '') {
  171. localInputs.other = '2023-06-01-preview';
  172. }
  173. if (localInputs.type === 18 && localInputs.other === '') {
  174. localInputs.other = 'v2.1';
  175. }
  176. if (localInputs.model_mapping === '') {
  177. localInputs.model_mapping = '{}';
  178. }
  179. let res;
  180. localInputs.models = localInputs.models.join(',');
  181. localInputs.group = localInputs.groups.join(',');
  182. if (isEdit) {
  183. res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) });
  184. } else {
  185. res = await API.post(`/api/channel/`, localInputs);
  186. }
  187. const { success, message } = res.data;
  188. if (success) {
  189. if (isEdit) {
  190. showSuccess('渠道更新成功!');
  191. } else {
  192. showSuccess('渠道创建成功!');
  193. setInputs(originInputs);
  194. }
  195. } else {
  196. showError(message);
  197. }
  198. };
  199. const addCustomModel = () => {
  200. if (customModel.trim() === '') return;
  201. if (inputs.models.includes(customModel)) return;
  202. let localModels = [...inputs.models];
  203. localModels.push(customModel);
  204. let localModelOptions = [];
  205. localModelOptions.push({
  206. key: customModel,
  207. text: customModel,
  208. value: customModel
  209. });
  210. setModelOptions(modelOptions => {
  211. return [...modelOptions, ...localModelOptions];
  212. });
  213. setCustomModel('');
  214. handleInputChange(null, { name: 'models', value: localModels });
  215. };
  216. return (
  217. <>
  218. <Segment loading={loading}>
  219. <Header as='h3'>{isEdit ? '更新渠道信息' : '创建新的渠道'}</Header>
  220. <Form autoComplete='new-password'>
  221. <Form.Field>
  222. <Form.Select
  223. label='类型'
  224. name='type'
  225. required
  226. options={CHANNEL_OPTIONS}
  227. value={inputs.type}
  228. onChange={handleInputChange}
  229. />
  230. </Form.Field>
  231. {
  232. inputs.type === 3 && (
  233. <>
  234. <Message>
  235. 注意,<strong>模型部署名称必须和模型名称保持一致</strong>,因为 One API 会把请求体中的 model
  236. 参数替换为你的部署名称(模型名称中的点会被剔除),<a target='_blank'
  237. href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'>图片演示</a>。
  238. </Message>
  239. <Form.Field>
  240. <Form.Input
  241. label='AZURE_OPENAI_ENDPOINT'
  242. name='base_url'
  243. placeholder={'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'}
  244. onChange={handleInputChange}
  245. value={inputs.base_url}
  246. autoComplete='new-password'
  247. />
  248. </Form.Field>
  249. <Form.Field>
  250. <Form.Input
  251. label='默认 API 版本'
  252. name='other'
  253. placeholder={'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'}
  254. onChange={handleInputChange}
  255. value={inputs.other}
  256. autoComplete='new-password'
  257. />
  258. </Form.Field>
  259. </>
  260. )
  261. }
  262. {
  263. inputs.type === 8 && (
  264. <Form.Field>
  265. <Form.Input
  266. label='Base URL'
  267. name='base_url'
  268. placeholder={'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'}
  269. onChange={handleInputChange}
  270. value={inputs.base_url}
  271. autoComplete='new-password'
  272. />
  273. </Form.Field>
  274. )
  275. }
  276. <Form.Field>
  277. <Form.Input
  278. label='名称'
  279. required
  280. name='name'
  281. placeholder={'请为渠道命名'}
  282. onChange={handleInputChange}
  283. value={inputs.name}
  284. autoComplete='new-password'
  285. />
  286. </Form.Field>
  287. <Form.Field>
  288. <Form.Dropdown
  289. label='分组'
  290. placeholder={'请选择可以使用该渠道的分组'}
  291. name='groups'
  292. required
  293. fluid
  294. multiple
  295. selection
  296. allowAdditions
  297. additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
  298. onChange={handleInputChange}
  299. value={inputs.groups}
  300. autoComplete='new-password'
  301. options={groupOptions}
  302. />
  303. </Form.Field>
  304. {
  305. inputs.type === 18 && (
  306. <Form.Field>
  307. <Form.Input
  308. label='模型版本'
  309. name='other'
  310. placeholder={'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'}
  311. onChange={handleInputChange}
  312. value={inputs.other}
  313. autoComplete='new-password'
  314. />
  315. </Form.Field>
  316. )
  317. }
  318. {
  319. inputs.type === 21 && (
  320. <Form.Field>
  321. <Form.Input
  322. label='知识库 ID'
  323. name='other'
  324. placeholder={'请输入知识库 ID,例如:123456'}
  325. onChange={handleInputChange}
  326. value={inputs.other}
  327. autoComplete='new-password'
  328. />
  329. </Form.Field>
  330. )
  331. }
  332. <Form.Field>
  333. <Form.Dropdown
  334. label='模型'
  335. placeholder={'请选择该渠道所支持的模型'}
  336. name='models'
  337. required
  338. fluid
  339. multiple
  340. selection
  341. onChange={handleInputChange}
  342. value={inputs.models}
  343. autoComplete='new-password'
  344. options={modelOptions}
  345. />
  346. </Form.Field>
  347. <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
  348. <Button type={'button'} onClick={() => {
  349. handleInputChange(null, { name: 'models', value: basicModels });
  350. }}>填入基础模型</Button>
  351. <Button type={'button'} onClick={() => {
  352. handleInputChange(null, { name: 'models', value: fullModels });
  353. }}>填入所有模型</Button>
  354. <Button type={'button'} onClick={() => {
  355. handleInputChange(null, { name: 'models', value: [] });
  356. }}>清除所有模型</Button>
  357. <Input
  358. action={
  359. <Button type={'button'} onClick={addCustomModel}>填入</Button>
  360. }
  361. placeholder='输入自定义模型名称'
  362. value={customModel}
  363. onChange={(e, { value }) => {
  364. setCustomModel(value);
  365. }}
  366. onKeyDown={(e) => {
  367. if (e.key === 'Enter') {
  368. addCustomModel();
  369. e.preventDefault();
  370. }
  371. }}
  372. />
  373. </div>
  374. <Form.Field>
  375. <Form.TextArea
  376. label='模型重定向'
  377. placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
  378. name='model_mapping'
  379. onChange={handleInputChange}
  380. value={inputs.model_mapping}
  381. style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
  382. autoComplete='new-password'
  383. />
  384. </Form.Field>
  385. {
  386. batch ? <Form.Field>
  387. <Form.TextArea
  388. label='密钥'
  389. name='key'
  390. required
  391. placeholder={'请输入密钥,一行一个'}
  392. onChange={handleInputChange}
  393. value={inputs.key}
  394. style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
  395. autoComplete='new-password'
  396. />
  397. </Form.Field> : <Form.Field>
  398. <Form.Input
  399. label='密钥'
  400. name='key'
  401. required
  402. placeholder={type2secretPrompt(inputs.type)}
  403. onChange={handleInputChange}
  404. value={inputs.key}
  405. autoComplete='new-password'
  406. />
  407. </Form.Field>
  408. }
  409. {
  410. !isEdit && (
  411. <Form.Checkbox
  412. checked={batch}
  413. label='批量创建'
  414. name='batch'
  415. onChange={() => setBatch(!batch)}
  416. />
  417. )
  418. }
  419. {
  420. inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && (
  421. <Form.Field>
  422. <Form.Input
  423. label='代理'
  424. name='base_url'
  425. placeholder={'此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com'}
  426. onChange={handleInputChange}
  427. value={inputs.base_url}
  428. autoComplete='new-password'
  429. />
  430. </Form.Field>
  431. )
  432. }
  433. {
  434. inputs.type === 22 && (
  435. <Form.Field>
  436. <Form.Input
  437. label='私有部署地址'
  438. name='base_url'
  439. placeholder={'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'}
  440. onChange={handleInputChange}
  441. value={inputs.base_url}
  442. autoComplete='new-password'
  443. />
  444. </Form.Field>
  445. )
  446. }
  447. <Button onClick={handleCancel}>取消</Button>
  448. <Button type={isEdit ? 'button' : 'submit'} positive onClick={submit}>提交</Button>
  449. </Form>
  450. </Segment>
  451. </>
  452. );
  453. };
  454. export default EditChannel;