EditChannel.js 16 KB

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