EditChannel.js 16 KB

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